diff --git a/contracts/RiskSteward/IRiskStewardReceiver.sol b/contracts/RiskSteward/IRiskStewardReceiver.sol new file mode 100644 index 00000000..7bfa508f --- /dev/null +++ b/contracts/RiskSteward/IRiskStewardReceiver.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; +import { IRiskOracle } from "../interfaces/IRiskOracle.sol"; +import { ICorePoolComptroller } from "../interfaces/ICorePoolComptroller.sol"; + +struct RiskParamConfig { + bool active; + uint256 debounce; + uint256 maxIncreaseBps; + bool isRelative; +} + +interface IRiskStewardReceiver { + function RISK_ORACLE() external view returns (IRiskOracle); + + function CORE_POOL_COMPTROLLER() external view returns (ICorePoolComptroller); + + function initialize() external; + + function setRiskParameterConfig( + string calldata updateType, + uint256 debounce, + uint256 maxIncreaseBps, + bool isRelative + ) external; + + function getRiskParameterConfig(string calldata updateType) external view returns (RiskParamConfig memory); + + function toggleConfigActive(string calldata updateType) external; + + function processUpdateById(uint256 updateId) external; + + function processUpdateByParameterAndMarket(string memory updateType, address market) external; +} diff --git a/contracts/RiskSteward/RiskStewardReceiver.sol b/contracts/RiskSteward/RiskStewardReceiver.sol new file mode 100644 index 00000000..a5c016ce --- /dev/null +++ b/contracts/RiskSteward/RiskStewardReceiver.sol @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { IRiskOracle, RiskParameterUpdate } from "../interfaces/IRiskOracle.sol"; +import { IVToken } from "../interfaces/IVToken.sol"; +import { ICorePoolComptroller } from "../interfaces/ICorePoolComptroller.sol"; +import { IIsolatedPoolsComptroller } from "../interfaces/IIsolatedPoolsComptroller.sol"; +import { IRiskStewardReceiver, RiskParamConfig } from "./IRiskStewardReceiver.sol"; + +/** + * @title RiskStewardReceiver + * @author Venus + * @notice Contract that can automatically adjust market caps based on risk oracle recommendations + * @custom:security-contact https://github.com/VenusProtocol/governance-contracts#discussion + */ +contract RiskStewardReceiver is IRiskStewardReceiver, Initializable, Ownable2StepUpgradeable { + /** + * @notice Mapping of supported risk configurations and their validation parameters + */ + mapping(string updateType => RiskParamConfig) private riskParameterConfigs; + + /** + * @notice Whitelisted oracle address to receive updates from + */ + IRiskOracle public immutable RISK_ORACLE; + + /** + * @notice Address of the CorePool comptroller used for selecting the correct comptroller abi + */ + ICorePoolComptroller public immutable CORE_POOL_COMPTROLLER; + + /** + * @notice Mapping of market and update type to last update. Used for debouncing updates. + */ + mapping(bytes marketUpdateType => uint256) public lastProcessedTime; + + /** + * @notice Mapping of processed updates. Used to prevent re-execution + */ + mapping(uint256 updateId => bool) public processedUpdates; + + /** + * @notice Time before a submitted update is considered stale + */ + uint256 public constant UPDATE_EXPIRATION_TIME = 1 days; + + /** + * @notice Event emitted when an update is successfully applied + */ + event RiskParameterUpdated(uint256 updateId); + + /** + * @notice Flag for if a risk parameter type can be updated by the steward + */ + error ConfigNotActive(); + + /** + * @notice Error for when an update was not applied within the required timeframe + */ + error UpdateIsExpired(); + + /** + * @notice Thrown when an update has already been processed + */ + error ConfigAlreadyProcessed(); + + /** + * @notice Thrown when the debounce period hasn't passed for applying an update to a specific market/ update type + */ + error UpdateTooFrequent(); + + /** + * @notice Thrown when the new value of an update is our of range + */ + error UpdateNotInRange(); + + /** + * @notice Thrown when a config that is not implemented is set + */ + error UnsupportedUpdateType(); + + /** + * @notice Flag to pause/unpause update processing + */ + bool public paused; + + /** + * @notice Event emitted when pause state is changed + */ + event PauseStateChanged(bool newState); + + /** + * @notice Error thrown when contract is paused + */ + error ContractPaused(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address riskOracle_, address corePoolComptroller_) { + require(riskOracle_ != address(0), "Risk Oracle address must not be zero"); + require(corePoolComptroller_ != address(0), "Core Pool Comptroller address must not be zero"); + RISK_ORACLE = IRiskOracle(riskOracle_); + CORE_POOL_COMPTROLLER = ICorePoolComptroller(corePoolComptroller_); + _disableInitializers(); + } + + function initialize() external initializer { + __Ownable2Step_init(); + } + + /** + * @notice Sets the risk parameter config for a given update type + * @param updateType The type of update to set the config for + * @param debounce The debounce period for the update + * @param maxIncreaseBps The max increase bps for the update + * @param isRelative Whether the max increase bps is relative or absolute + */ + function setRiskParameterConfig( + string calldata updateType, + uint256 debounce, + uint256 maxIncreaseBps, + bool isRelative + ) external onlyOwner { + riskParameterConfigs[updateType] = RiskParamConfig({ + active: true, + debounce: debounce, + maxIncreaseBps: maxIncreaseBps, + isRelative: isRelative + }); + } + + /** + * @notice Gets the risk parameter config for a given update type + * @param updateType The type of update to get the config for + * @return The risk parameter config for the given update type + */ + function getRiskParameterConfig(string calldata updateType) external view returns (RiskParamConfig memory) { + return riskParameterConfigs[updateType]; + } + + /** + * @notice Toggles the active status of a risk parameter config + * @param updateType The type of update to toggle the config for + */ + function toggleConfigActive(string calldata updateType) external onlyOwner { + riskParameterConfigs[updateType].active = !riskParameterConfigs[updateType].active; + } + + /** + * @notice Processes an update by its ID + * @param updateId The ID of the update to process + * @custom:event Emits RiskParameterUpdated with the update ID + * @custom:error Throws ConfigNotActive if the config is not active + * @custom:error Throws UpdateIsExpired if the update is expired + * @custom:error Throws ConfigAlreadyProcessed if the update has already been processed + * @custom:error Throws UpdateTooFrequent if the update is too frequent + * @custom:error Throws UpdateNotInRange if the update is not in range + * @custom:error Throws UnsupportedUpdateType if the update type is not supported + */ + function processUpdateById(uint256 updateId) external whenNotPaused { + RiskParameterUpdate memory update = RISK_ORACLE.getUpdateById(updateId); + _validateUpdateStatus(update); + _processUpdate(update); + } + + /** + * @notice Processes the latest update for a given parameter and market + * @param updateType The type of update to process + * @param market The market to process the update for + */ + function processUpdateByParameterAndMarket(string memory updateType, address market) external whenNotPaused { + RiskParameterUpdate memory update = RISK_ORACLE.getLatestUpdateByParameterAndMarket(updateType, market); + _validateUpdateStatus(update); + _processUpdate(update); + } + + function _processUpdate(RiskParameterUpdate memory update) internal { + if (Strings.equal(update.updateType, "MarketSupplyCaps")) { + _processSupplyCapUpdate(update); + } else if (Strings.equal(update.updateType, "MarketBorrowCaps")) { + _processBorrowCapUpdate(update); + } else { + revert UnsupportedUpdateType(); + } + lastProcessedTime[_getMarketUpdateTypeKey(update.market, update.updateType)] = block.timestamp; + processedUpdates[update.updateId] = true; + emit RiskParameterUpdated(update.updateId); + } + + function _getMarketUpdateTypeKey(address market, string memory updateType) internal pure returns (bytes memory) { + return abi.encodePacked(market, updateType); + } + + function _updateSupplyCaps(address market, bytes memory newValue) internal { + address comptroller = IVToken(market).comptroller(); + address[] memory newSupplyCapMarkets = new address[](1); + newSupplyCapMarkets[0] = market; + uint256[] memory newSupplyCaps = new uint256[](1); + + newSupplyCaps[0] = uint256(bytes32(newValue)); + if (comptroller == address(CORE_POOL_COMPTROLLER)) { + ICorePoolComptroller(comptroller)._setMarketSupplyCaps(newSupplyCapMarkets, newSupplyCaps); + } else { + IIsolatedPoolsComptroller(comptroller).setMarketSupplyCaps(newSupplyCapMarkets, newSupplyCaps); + } + } + + function _updateBorrowCaps(address market, bytes memory newValue) internal { + address comptroller = IVToken(market).comptroller(); + address[] memory newBorrowCapMarkets = new address[](1); + newBorrowCapMarkets[0] = market; + uint256[] memory newBorrowCaps = new uint256[](1); + newBorrowCaps[0] = uint256(bytes32(newValue)); + if (comptroller == address(CORE_POOL_COMPTROLLER)) { + ICorePoolComptroller(comptroller)._setMarketBorrowCaps(newBorrowCapMarkets, newBorrowCaps); + } else { + IIsolatedPoolsComptroller(comptroller).setMarketBorrowCaps(newBorrowCapMarkets, newBorrowCaps); + } + } + + function _processSupplyCapUpdate(RiskParameterUpdate memory update) internal { + _validateSupplyCapUpdate(update); + _updateSupplyCaps(update.market, update.newValue); + } + + function _processBorrowCapUpdate(RiskParameterUpdate memory update) internal { + _validateBorrowCapUpdate(update); + _updateBorrowCaps(update.market, update.newValue); + } + + function _validateUpdateStatus(RiskParameterUpdate memory update) internal view { + RiskParamConfig memory config = riskParameterConfigs[update.updateType]; + + if (!config.active) { + revert ConfigNotActive(); + } + + if (update.timestamp + UPDATE_EXPIRATION_TIME < block.timestamp) { + revert UpdateIsExpired(); + } + + if (processedUpdates[update.updateId]) { + revert ConfigAlreadyProcessed(); + } + + if ( + block.timestamp - lastProcessedTime[_getMarketUpdateTypeKey(update.market, update.updateType)] < + config.debounce + ) { + revert UpdateTooFrequent(); + } + } + + function _validateSupplyCapUpdate(RiskParameterUpdate memory update) internal view { + RiskParamConfig memory config = riskParameterConfigs[update.updateType]; + + ICorePoolComptroller comptroller = ICorePoolComptroller(IVToken(update.market).comptroller()); + uint256 currentSupplyCap = comptroller.supplyCaps(address(update.market)); + + uint256 newValue = uint256(bytes32(update.newValue)); + _updateWithinAllowedRange(currentSupplyCap, newValue, config.maxIncreaseBps, config.isRelative); + } + + function _validateBorrowCapUpdate(RiskParameterUpdate memory update) internal view { + RiskParamConfig memory config = riskParameterConfigs[update.updateType]; + + ICorePoolComptroller comptroller = ICorePoolComptroller(IVToken(update.market).comptroller()); + uint256 currentBorrowCap = comptroller.borrowCaps(address(update.market)); + + uint256 newValue = uint256(bytes32(update.newValue)); + _updateWithinAllowedRange(currentBorrowCap, newValue, config.maxIncreaseBps, config.isRelative); + } + + /** + * @notice Ensures the risk param update is within the allowed range + * @param previousValue current risk param value + * @param newValue new updated risk param value + * @param maxIncreaseBps the max bps change allowed + * @param isRelative true, if maxPercentChange is relative in value, false if maxPercentChange + * is absolute in value. + * @return bool true, if difference is within the maxPercentChange + */ + function _updateWithinAllowedRange( + uint256 previousValue, + uint256 newValue, + uint256 maxIncreaseBps, + bool isRelative + ) internal pure returns (bool) { + if (newValue < previousValue) { + revert UpdateNotInRange(); + } + + uint256 diff = newValue - previousValue; + + uint256 maxDiff = isRelative ? (maxIncreaseBps * previousValue) / 10000 : maxIncreaseBps; + + if (diff > maxDiff) { + revert UpdateNotInRange(); + } + } + + /** + * @notice Toggles the pause state of the contract + * @custom:event Emits PauseStateChanged + */ + function togglePaused() external onlyOwner { + paused = !paused; + emit PauseStateChanged(paused); + } + + /** + * @notice Modifier to prevent execution when contract is paused + */ + modifier whenNotPaused() { + if (paused) revert ContractPaused(); + _; + } + + uint256[50] private __gap; +} diff --git a/contracts/interfaces/ICorePoolComptroller.sol b/contracts/interfaces/ICorePoolComptroller.sol new file mode 100644 index 00000000..17bbef64 --- /dev/null +++ b/contracts/interfaces/ICorePoolComptroller.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +interface ICorePoolComptroller { + function borrowCaps(address) external view returns (uint256); + + function supplyCaps(address) external view returns (uint256); + + function _setMarketSupplyCaps(address[] calldata, uint256[] calldata) external; + + function _setMarketBorrowCaps(address[] calldata, uint256[] calldata) external; +} diff --git a/contracts/interfaces/IIsolatedPoolsComptroller.sol b/contracts/interfaces/IIsolatedPoolsComptroller.sol new file mode 100644 index 00000000..bb227ead --- /dev/null +++ b/contracts/interfaces/IIsolatedPoolsComptroller.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +interface IIsolatedPoolsComptroller { + function borrowCaps(address) external view returns (uint256); + + function supplyCaps(address) external view returns (uint256); + + function setMarketSupplyCaps(address[] calldata, uint256[] calldata) external; + + function setMarketBorrowCaps(address[] calldata, uint256[] calldata) external; +} diff --git a/contracts/interfaces/IRiskOracle.sol b/contracts/interfaces/IRiskOracle.sol new file mode 100644 index 00000000..55be7a53 --- /dev/null +++ b/contracts/interfaces/IRiskOracle.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +struct RiskParameterUpdate { + uint256 timestamp; // Timestamp of the update + bytes newValue; // Encoded parameters, flexible for various data types + string referenceId; // External reference, potentially linking to a document or off-chain data + bytes previousValue; // Previous value of the parameter for historical comparison + string updateType; // Classification of the update for validation purposes + uint256 updateId; // Unique identifier for this specific update + address market; // Address for market of the parameter update + bytes additionalData; // Additional data for the update +} + +interface IRiskOracle { + function addAuthorizedSender(address sender) external; + + function removeAuthorizedSender(address sender) external; + + function addUpdateType(string memory newUpdateType) external; + + function publishRiskParameterUpdate( + string memory referenceId, + bytes memory newValue, + string memory updateType, + address market, + bytes memory additionalData + ) external; + + function publishBulkRiskParameterUpdates( + string[] memory referenceIds, + bytes[] memory newValues, + string[] memory updateTypes, + address[] memory markets, + bytes[] memory additionalData + ) external; + + function getAllUpdateTypes() external view returns (string[] memory); + + function getLatestUpdateByParameterAndMarket( + string memory updateType, + address market + ) external view returns (RiskParameterUpdate memory); + + function getUpdateById(uint256 updateId) external view returns (RiskParameterUpdate memory); + + function isAuthorized(address sender) external view returns (bool); +} diff --git a/contracts/interfaces/IVToken.sol b/contracts/interfaces/IVToken.sol new file mode 100644 index 00000000..80ba2f83 --- /dev/null +++ b/contracts/interfaces/IVToken.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +interface IVToken { + function comptroller() external view returns (address); +} diff --git a/contracts/test/MockComptroller.sol b/contracts/test/MockComptroller.sol new file mode 100644 index 00000000..b49d7b8f --- /dev/null +++ b/contracts/test/MockComptroller.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { IVToken } from "../interfaces/IVToken.sol"; + +contract MockComptroller { + /// @notice Mapping of vToken addresses to their supply caps + mapping(address => uint256) public supplyCaps; + + /// @notice Mapping of vToken addresses to their borrow caps + mapping(address => uint256) public borrowCaps; + + /// @notice Array of all vTokens + IVToken[] public allVTokens; + + /// @notice Mapping of vToken addresses to boolean indicating if they are listed + mapping(address => bool) public vTokenListed; + + /** + * @notice Add a new vToken to be tracked + * @param vToken The vToken to add + */ + function supportMarket(address vToken) external { + require(!vTokenListed[vToken], "vToken already listed"); + vTokenListed[address(vToken)] = true; + allVTokens.push(IVToken(vToken)); + } + + /** + * @notice Set the supply cap for a vToken + * @param vTokens The vToken addresses + * @param newCaps The new supply caps + */ + function setMarketSupplyCaps(address[] calldata vTokens, uint256[] calldata newCaps) external { + uint256 numMarkets = vTokens.length; + uint256 numSupplyCaps = newCaps.length; + + require(numMarkets != 0 && numMarkets == numSupplyCaps, "invalid input"); + + for (uint256 i; i < numMarkets; ++i) { + require(vTokenListed[vTokens[i]], "vToken not listed"); + supplyCaps[address(vTokens[i])] = newCaps[i]; + } + } + + /** + * @notice Set the borrow cap for a vToken + * @param vTokens The vToken addresses + * @param newCaps The new borrow caps + */ + function setMarketBorrowCaps(address[] calldata vTokens, uint256[] calldata newCaps) external { + uint256 numMarkets = vTokens.length; + uint256 numBorrowCaps = newCaps.length; + + require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); + + for (uint256 i; i < numMarkets; ++i) { + require(vTokenListed[vTokens[i]], "vToken not listed"); + borrowCaps[address(vTokens[i])] = newCaps[i]; + } + } + + /** + * @notice Get all vTokens + * @return Array of vToken addresses + */ + function getAllMarkets() external view returns (IVToken[] memory) { + return allVTokens; + } +} diff --git a/contracts/test/MockCoreComptroller.sol b/contracts/test/MockCoreComptroller.sol new file mode 100644 index 00000000..86af9311 --- /dev/null +++ b/contracts/test/MockCoreComptroller.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { IVToken } from "../interfaces/IVToken.sol"; + +contract MockCoreComptroller { + /// @notice Mapping of vToken addresses to their supply caps + mapping(address => uint256) public supplyCaps; + + /// @notice Mapping of vToken addresses to their borrow caps + mapping(address => uint256) public borrowCaps; + + /// @notice Array of all vTokens + IVToken[] public allVTokens; + + /// @notice Mapping of vToken addresses to boolean indicating if they are listed + mapping(address => bool) public vTokenListed; + + /** + * @notice Add a new vToken to be tracked + * @param vToken The vToken to add + */ + function _supportMarket(address vToken) external { + require(!vTokenListed[vToken], "vToken already listed"); + vTokenListed[address(vToken)] = true; + allVTokens.push(IVToken(vToken)); + } + + /** + * @notice Set the supply cap for a vToken + * @param vTokens The vToken addresses + * @param newCaps The new supply caps + */ + function _setMarketSupplyCaps(address[] calldata vTokens, uint256[] calldata newCaps) external { + uint256 numMarkets = vTokens.length; + uint256 numSupplyCaps = newCaps.length; + + require(numMarkets != 0 && numMarkets == numSupplyCaps, "invalid input"); + + for (uint256 i; i < numMarkets; ++i) { + require(vTokenListed[vTokens[i]], "vToken not listed"); + supplyCaps[address(vTokens[i])] = newCaps[i]; + } + } + + /** + * @notice Set the borrow cap for a vToken + * @param vTokens The vToken addresses + * @param newCaps The new borrow caps + */ + function _setMarketBorrowCaps(address[] calldata vTokens, uint256[] calldata newCaps) external { + uint256 numMarkets = vTokens.length; + uint256 numBorrowCaps = newCaps.length; + + require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); + + for (uint256 i; i < numMarkets; ++i) { + require(vTokenListed[vTokens[i]], "vToken not listed"); + borrowCaps[address(vTokens[i])] = newCaps[i]; + } + } + + /** + * @notice Get all vTokens + * @return Array of vToken addresses + */ + function getAllMarkets() external view returns (IVToken[] memory) { + return allVTokens; + } +} diff --git a/contracts/test/MockRiskOracle.sol b/contracts/test/MockRiskOracle.sol new file mode 100644 index 00000000..c0256432 --- /dev/null +++ b/contracts/test/MockRiskOracle.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +/** + * @title Risk Oracle + * @author Chaos Labs + */ +contract MockRiskOracle is Ownable { + struct RiskParameterUpdate { + uint256 timestamp; // Timestamp of the update + bytes newValue; // Encoded parameters, flexible for various data types + string referenceId; // External reference, potentially linking to a document or off-chain data + bytes previousValue; // Previous value of the parameter for historical comparison + string updateType; // Classification of the update for validation purposes + uint256 updateId; // Unique identifier for this specific update + address market; // Address for market of the parameter update + bytes additionalData; // Additional data for the update + } + + string[] private allUpdateTypes; // Array to store all update types + mapping(string => bool) internal validUpdateTypes; // Whitelist of valid update type identifiers + mapping(uint256 => RiskParameterUpdate) private updatesById; // Mapping from unique update ID to the update details + mapping(address => bool) private authorizedSenders; // Authorized accounts capable of executing updates + + mapping(address => mapping(string => uint256)) public latestUpdateIdByMarketAndType; // Mapping to store the latest update ID for each combination of market and update type + uint256 public updateCounter; // Counter to keep track of the total number of updates + string public description; // Description of contract + + event ParameterUpdated( + string referenceId, + bytes newValue, + bytes previousValue, + uint256 timestamp, + string indexed updateType, + uint256 indexed updateId, + address indexed market, + bytes additionalData + ); + + event AuthorizedSenderAdded(address indexed sender); + event AuthorizedSenderRemoved(address indexed sender); + event UpdateTypeAdded(string indexed updateType); + + modifier onlyAuthorized() { + require(authorizedSenders[msg.sender], "Unauthorized: Sender not authorized."); + _; + } + + modifier onlyValidString(string memory input) { + require(bytes(input).length > 0 && bytes(input).length <= 64, "Invalid update type string"); + _; + } + + /** + * @notice Constructor to set initial authorized addresses and approved update types. + * @param _description Description of contract + * @param initialSenders List of addresses that will initially be authorized to perform updates. + * @param initialUpdateTypes List of valid update types initially allowed. + */ + constructor( + string memory _description, + address[] memory initialSenders, + string[] memory initialUpdateTypes + ) Ownable() { + description = _description; + for (uint256 i = 0; i < initialSenders.length; i++) { + authorizedSenders[initialSenders[i]] = true; // Automatically authorize initial senders + } + for (uint256 i = 0; i < initialUpdateTypes.length; i++) { + if (!validUpdateTypes[initialUpdateTypes[i]]) { + // Ensure no duplicate updateTypes can be set + validUpdateTypes[initialUpdateTypes[i]] = true; // Register initial valid updates + allUpdateTypes.push(initialUpdateTypes[i]); + emit UpdateTypeAdded(initialUpdateTypes[i]); + } + } + } + + /** + * @notice Adds a new sender to the list of addresses authorized to perform updates. + * @param sender Address to be authorized. + */ + function addAuthorizedSender(address sender) external onlyOwner { + require(!authorizedSenders[sender], "Sender already authorized."); + authorizedSenders[sender] = true; + emit AuthorizedSenderAdded(sender); + } + + /** + * @notice Removes an address from the list of authorized senders. + * @param sender Address to be unauthorized. + */ + function removeAuthorizedSender(address sender) external onlyOwner { + require(authorizedSenders[sender], "Sender not authorized."); + authorizedSenders[sender] = false; + emit AuthorizedSenderRemoved(sender); + } + + /** + * @notice Adds a new type of update to the list of authorized update types. + * @param newUpdateType New type of update to allow. + */ + function addUpdateType(string memory newUpdateType) external onlyOwner onlyValidString(newUpdateType) { + require(!validUpdateTypes[newUpdateType], "Update type already exists."); + validUpdateTypes[newUpdateType] = true; + allUpdateTypes.push(newUpdateType); + emit UpdateTypeAdded(newUpdateType); + } + + /** + * @notice Publishes a new risk parameter update. + * @param referenceId An external reference ID associated with the update. + * @param newValue The new value of the risk parameter being updated. + * @param updateType Type of update performed, must be previously authorized. + * @param market Address for market of the parameter update + * @param additionalData Additional data for the update + */ + function publishRiskParameterUpdate( + string memory referenceId, + bytes memory newValue, + string memory updateType, + address market, + bytes memory additionalData + ) external onlyAuthorized { + _processUpdate(referenceId, newValue, updateType, market, additionalData); + } + + /** + * @notice Publishes multiple risk parameter updates in a single transaction. + * @param referenceIds Array of external reference IDs. + * @param newValues Array of new values for each update. + * @param updateTypes Array of types for each update, all must be authorized. + * @param markets Array of addresses for markets of the parameter updates + * @param additionalData Array of additional data for the updates + * + */ + function publishBulkRiskParameterUpdates( + string[] memory referenceIds, + bytes[] memory newValues, + string[] memory updateTypes, + address[] memory markets, + bytes[] memory additionalData + ) external onlyAuthorized { + for (uint256 i = 0; i < referenceIds.length; i++) { + _processUpdate(referenceIds[i], newValues[i], updateTypes[i], markets[i], additionalData[i]); + } + } + + /** + * @dev Processes an update internally, recording and emitting an event. + */ + function _processUpdate( + string memory referenceId, + bytes memory newValue, + string memory updateType, + address market, + bytes memory additionalData + ) internal { + require(validUpdateTypes[updateType], "Unauthorized update type."); + updateCounter++; + uint256 previousUpdateId = latestUpdateIdByMarketAndType[market][updateType]; + bytes memory previousValue = updatesById[previousUpdateId].newValue; + + RiskParameterUpdate memory newUpdate = RiskParameterUpdate( + block.timestamp, + newValue, + referenceId, + previousValue, + updateType, + updateCounter, + market, + additionalData + ); + updatesById[updateCounter] = newUpdate; + + // Update the latest update ID for the market and updateType combination + latestUpdateIdByMarketAndType[market][updateType] = updateCounter; + + emit ParameterUpdated( + referenceId, + newValue, + previousValue, + block.timestamp, + updateType, + updateCounter, + market, + additionalData + ); + } + + function getAllUpdateTypes() external view returns (string[] memory) { + return allUpdateTypes; + } + + /** + * @notice Fetches the most recent update for a specific parameter in a specific market. + * @param updateType The identifier for the parameter. + * @param market The market identifier. + * @return The most recent RiskParameterUpdate for the specified parameter and market. + */ + function getLatestUpdateByParameterAndMarket( + string memory updateType, + address market + ) external view returns (RiskParameterUpdate memory) { + uint256 updateId = latestUpdateIdByMarketAndType[market][updateType]; + require(updateId > 0, "No update found for the specified parameter and market."); + return updatesById[updateId]; + } + + /* + * @notice Fetches the update for a provided updateId. + * @param updateId Update ID. + * @return The most recent RiskParameterUpdate for the specified id. + */ + function getUpdateById(uint256 updateId) external view returns (RiskParameterUpdate memory) { + require(updateId > 0 && updateId <= updateCounter, "Invalid update ID."); + return updatesById[updateId]; + } + + /** + * @notice Checks if an address is authorized to perform updates. + * @param sender Address to check. + * @return Boolean indicating whether the address is authorized. + */ + function isAuthorized(address sender) external view returns (bool) { + return authorizedSenders[sender]; + } +} diff --git a/contracts/test/MockVToken.sol b/contracts/test/MockVToken.sol new file mode 100644 index 00000000..b1a071ef --- /dev/null +++ b/contracts/test/MockVToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import "../interfaces/IVToken.sol"; + +contract MockVToken is IVToken { + address public override comptroller; + + constructor(address _comptroller) { + comptroller = _comptroller; + } +} diff --git a/package.json b/package.json index 42f9a165..88ac54e0 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@nomicfoundation/hardhat-verify": "^2.0.8", "@nomiclabs/hardhat-ethers": "^2.2.1", "@nomiclabs/hardhat-etherscan": "^3.1.2", - "@openzeppelin/contracts": "^4.8.2", + "@openzeppelin/contracts": "^4.9.6", "@openzeppelin/contracts-upgradeable": "^4.8.2", "@openzeppelin/hardhat-upgrades": "^1.22.1", "@semantic-release/changelog": "^6.0.2", diff --git a/tests/RiskSteward/RiskStewardReceiver.ts b/tests/RiskSteward/RiskStewardReceiver.ts new file mode 100644 index 00000000..b03c4675 --- /dev/null +++ b/tests/RiskSteward/RiskStewardReceiver.ts @@ -0,0 +1,415 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { BigNumber, ContractFactory } from "ethers"; +import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { MockComptroller, MockCoreComptroller, MockRiskOracle, MockVToken, RiskStewardReceiver } from "../../typechain"; + +const { parseUnits, hexValue } = ethers.utils; + +const parseUnitsToHex = (value: number) => { + return ethers.utils.hexZeroPad(hexValue(BigNumber.from(parseUnits(value.toString(), 18))), 32); +}; + +describe("Risk Steward", async function () { + let deployer: SignerWithAddress, + mockRiskOracle: MockRiskOracle, + riskStewardReceiver: RiskStewardReceiver, + RiskStewardReceiverFactory: ContractFactory, + MockRiskOracleFactory: ContractFactory, + mockCoreVToken: MockVToken, + mockVToken: MockVToken, + mockCoreComptroller: MockCoreComptroller, + mockComptroller: MockComptroller; + + const riskStewardFixture = async () => { + deployer = (await ethers.getSigners())[0]; + + // Set up mock comptroller and markets + const MockVTokenFactory = await ethers.getContractFactory("MockVToken"); + // Core Pool Comptroller + const MockCoreComptrollerFactory = await ethers.getContractFactory("MockCoreComptroller"); + mockCoreComptroller = await MockCoreComptrollerFactory.deploy(); + mockCoreVToken = await MockVTokenFactory.deploy(mockCoreComptroller.address); + mockCoreComptroller._supportMarket(mockCoreVToken.address); + mockCoreComptroller._setMarketSupplyCaps([mockCoreVToken.address], [parseUnits("8", 18)]); + mockCoreComptroller._setMarketBorrowCaps([mockCoreVToken.address], [parseUnits("8", 18)]); + + // IL Comptroller + const MockComptrollerFactory = await ethers.getContractFactory("MockComptroller"); + mockComptroller = await MockComptrollerFactory.deploy(); + mockVToken = await MockVTokenFactory.deploy(mockComptroller.address); + + mockComptroller.supportMarket(mockVToken.address); + mockComptroller.setMarketSupplyCaps([mockVToken.address], [parseUnits("8", 18)]); + mockComptroller.setMarketBorrowCaps([mockVToken.address], [parseUnits("8", 18)]); + MockRiskOracleFactory = await ethers.getContractFactory("MockRiskOracle"); + mockRiskOracle = (await MockRiskOracleFactory.deploy( + "Mock Risk Oracle", + [deployer.address], + ["MarketSupplyCaps", "MarketBorrowCaps", "RandomUpdateType"], + )) as MockRiskOracle; + RiskStewardReceiverFactory = await ethers.getContractFactory("RiskStewardReceiver"); + riskStewardReceiver = await upgrades.deployProxy(RiskStewardReceiverFactory, [], { + constructorArgs: [mockRiskOracle.address, mockCoreComptroller.address], + initializer: "initialize", + unsafeAllow: ["state-variable-immutable"], + }); + + await riskStewardReceiver.setRiskParameterConfig("MarketSupplyCaps", 5, 5000, true); + await riskStewardReceiver.setRiskParameterConfig("MarketBorrowCaps", 5, 5000, true); + }; + + beforeEach(async function () { + await loadFixture(riskStewardFixture); + }); + + describe("Upgradeable", async function () { + it("new implementation should update core comptroller", async function () { + const corePoolComptrollerTestnetAddress = "0x94d1820b2D1c7c7452A163983Dc888CEC546b77D"; + await upgrades.upgradeProxy(riskStewardReceiver, RiskStewardReceiverFactory, { + constructorArgs: [mockRiskOracle.address, corePoolComptrollerTestnetAddress], + unsafeAllow: ["state-variable-immutable"], + }); + expect(await riskStewardReceiver.CORE_POOL_COMPTROLLER()).to.equal(corePoolComptrollerTestnetAddress); + }); + + it("new implementation should update risk oracle", async function () { + const mockRiskOracle = (await MockRiskOracleFactory.deploy( + "Mock Risk Oracle", + [deployer.address], + ["MarketSupplyCaps", "MarketBorrowCaps"], + )) as MockRiskOracle; + await upgrades.upgradeProxy(riskStewardReceiver, RiskStewardReceiverFactory, { + constructorArgs: [mockRiskOracle.address, mockCoreComptroller.address], + unsafeAllow: ["state-variable-immutable"], + }); + expect(await riskStewardReceiver.RISK_ORACLE()).to.equal(mockRiskOracle.address); + }); + }); + + describe("Risk Parameter Config", async function () { + it("should get original risk parameter configs", async function () { + expect(await riskStewardReceiver.getRiskParameterConfig("MarketSupplyCaps")).to.deep.equal([ + true, + BigNumber.from(5), + BigNumber.from(5000), + true, + ]); + expect(await riskStewardReceiver.getRiskParameterConfig("MarketBorrowCaps")).to.deep.equal([ + true, + BigNumber.from(5), + BigNumber.from(5000), + true, + ]); + }); + + it("should pause risk parameter configs", async function () { + await riskStewardReceiver.toggleConfigActive("MarketSupplyCaps"); + expect((await riskStewardReceiver.getRiskParameterConfig("MarketSupplyCaps")).active).to.equal(false); + }); + + it("should update risk parameter configs", async function () { + await riskStewardReceiver.setRiskParameterConfig("MarketSupplyCaps", 1, 1000, true); + expect(await riskStewardReceiver.getRiskParameterConfig("MarketSupplyCaps")).to.deep.equal([ + true, + BigNumber.from(1), + BigNumber.from(1000), + true, + ]); + }); + }); + + describe("Risk Steward Pause", async function () { + it("should toggle paused state", async function () { + await riskStewardReceiver.togglePaused(); + expect(await riskStewardReceiver.paused()).to.equal(true); + }); + + it("should revert if contract is paused", async function () { + await riskStewardReceiver.togglePaused(); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("ContractPaused"); + }); + + it("should revert if contract is paused", async function () { + await riskStewardReceiver.togglePaused(); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("ContractPaused"); + }); + }); + + describe("Risk Parameter Update Reverts under incorrect conditions", async function () { + it("should revert if updateType is unknown", async function () { + await expect( + mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnits("10", 18), + "UnknownUpdateType", + mockCoreVToken.address, + "0x", + ), + ).to.be.revertedWith("Unauthorized update type."); + }); + + it("should revert if updateType is implemented", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnits("10", 18), + "RandomUpdateType", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.setRiskParameterConfig("RandomUpdateType", 5, 5000, true); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UnsupportedUpdateType"); + }); + + it("should revert if updateType is not active", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.toggleConfigActive("MarketSupplyCaps"); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("ConfigNotActive"); + + await riskStewardReceiver.toggleConfigActive("MarketBorrowCaps"); + await expect(riskStewardReceiver.processUpdateById(2)).to.be.rejectedWith("ConfigNotActive"); + }); + + it("should revert if the update is expired", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await time.increase(60 * 60 * 24 + 1); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UpdateIsExpired"); + await expect(riskStewardReceiver.processUpdateById(2)).to.be.rejectedWith("UpdateIsExpired"); + }); + + it("should revert if market is not supported", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreComptroller.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.reverted; + + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreComptroller.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(2)).to.be.reverted; + }); + + it("should revert if the update is too frequent", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(12), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateById(1); + await expect(riskStewardReceiver.processUpdateById(2)).to.be.rejectedWith("UpdateTooFrequent"); + + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(12), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateById(3); + await expect(riskStewardReceiver.processUpdateById(4)).to.be.rejectedWith("UpdateTooFrequent"); + }); + + it("should error on invalid update ID", async function () { + await expect(riskStewardReceiver.processUpdateById(1)).to.be.revertedWith("Invalid update ID."); + }); + + it("should revert if the update has already been applied", async function () { + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateById(1); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("ConfigAlreadyProcessed"); + }); + + it("should revert if the update is out of bounds", async function () { + // Lower + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(5), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UpdateNotInRange"); + + // Too high + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(20), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UpdateNotInRange"); + + // Lower + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(5), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UpdateNotInRange"); + + // Too high + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(20), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await expect(riskStewardReceiver.processUpdateById(1)).to.be.rejectedWith("UpdateNotInRange"); + }); + }); + + describe("Risk Parameter Updates under correct conditions", async function () { + it("should process update by id", async function () { + // Core Pool + expect(await mockCoreComptroller.supplyCaps(mockCoreVToken.address)).to.equal(parseUnits("8", 18)); + expect(await mockCoreComptroller.borrowCaps(mockCoreVToken.address)).to.equal(parseUnits("8", 18)); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateById(1); + await riskStewardReceiver.processUpdateById(2); + expect(await mockCoreComptroller.supplyCaps(mockCoreVToken.address)).to.equal(parseUnits("10", 18)); + expect(await mockCoreComptroller.borrowCaps(mockCoreVToken.address)).to.equal(parseUnits("10", 18)); + // Isolated Pool + expect(await mockComptroller.supplyCaps(mockVToken.address)).to.equal(parseUnits("8", 18)); + expect(await mockComptroller.borrowCaps(mockVToken.address)).to.equal(parseUnits("8", 18)); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateById(3); + await riskStewardReceiver.processUpdateById(4); + expect(await mockComptroller.supplyCaps(mockVToken.address)).to.equal(parseUnits("10", 18)); + expect(await mockComptroller.borrowCaps(mockVToken.address)).to.equal(parseUnits("10", 18)); + }); + + it("should process update by parameter and market", async function () { + // Core Pool + expect(await mockCoreComptroller.supplyCaps(mockCoreVToken.address)).to.equal(parseUnits("8", 18)); + expect(await mockCoreComptroller.borrowCaps(mockCoreVToken.address)).to.equal(parseUnits("8", 18)); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockCoreVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockCoreVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateByParameterAndMarket("MarketSupplyCaps", mockCoreVToken.address); + await riskStewardReceiver.processUpdateByParameterAndMarket("MarketBorrowCaps", mockCoreVToken.address); + expect(await mockCoreComptroller.supplyCaps(mockCoreVToken.address)).to.equal(parseUnits("10", 18)); + expect(await mockCoreComptroller.borrowCaps(mockCoreVToken.address)).to.equal(parseUnits("10", 18)); + // Isolated Pool + expect(await mockComptroller.supplyCaps(mockVToken.address)).to.equal(parseUnits("8", 18)); + expect(await mockComptroller.borrowCaps(mockVToken.address)).to.equal(parseUnits("8", 18)); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketSupplyCaps", + mockVToken.address, + "0x", + ); + await mockRiskOracle.publishRiskParameterUpdate( + "ipfs://QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdw8eX9", + parseUnitsToHex(10), + "MarketBorrowCaps", + mockVToken.address, + "0x", + ); + await riskStewardReceiver.processUpdateByParameterAndMarket("MarketSupplyCaps", mockVToken.address); + await riskStewardReceiver.processUpdateByParameterAndMarket("MarketBorrowCaps", mockVToken.address); + expect(await mockComptroller.supplyCaps(mockVToken.address)).to.equal(parseUnits("10", 18)); + expect(await mockComptroller.borrowCaps(mockVToken.address)).to.equal(parseUnits("10", 18)); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index a53a4da9..79620aff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2456,20 +2456,13 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts@npm:^4.4.1, @openzeppelin/contracts@npm:^4.8.3, @openzeppelin/contracts@npm:^4.9.2": +"@openzeppelin/contracts@npm:^4.4.1, @openzeppelin/contracts@npm:^4.8.3, @openzeppelin/contracts@npm:^4.9.2, @openzeppelin/contracts@npm:^4.9.6": version: 4.9.6 resolution: "@openzeppelin/contracts@npm:4.9.6" checksum: 274b6e968268294f12d5ca4f0278f6e6357792c8bb4d76664f83dbdc325f780541538a127e6a6e97e4f018088b42f65952014dec9c745c0fa25081f43ef9c4bf languageName: node linkType: hard -"@openzeppelin/contracts@npm:^4.8.2": - version: 4.8.2 - resolution: "@openzeppelin/contracts@npm:4.8.2" - checksum: 1d362f0b9c880549cb82544e23fb70270fbbbe24a69e10bd5aa07649fd82347686173998ae484defafdc473d04004d519f839e3cd3d3e7733d0895b950622243 - languageName: node - linkType: hard - "@openzeppelin/defender-base-client@npm:^1.46.0": version: 1.54.1 resolution: "@openzeppelin/defender-base-client@npm:1.54.1" @@ -3450,29 +3443,7 @@ __metadata: languageName: node linkType: hard -"@venusprotocol/governance-contracts@npm:^1.4.0": - version: 1.4.0 - resolution: "@venusprotocol/governance-contracts@npm:1.4.0" - dependencies: - "@venusprotocol/solidity-utilities": ^1.1.0 - hardhat-deploy-ethers: ^0.3.0-beta.13 - module-alias: ^2.2.2 - checksum: 85c6b6a815edb0befa4c38e3652a58464827d390620210b99575c16960ee6505e95e7c2192ebc972da7ed758d3c62e150d32fbdd1f01acab1731f29b11d1884e - languageName: node - linkType: hard - -"@venusprotocol/governance-contracts@npm:^2.0.0": - version: 2.6.0 - resolution: "@venusprotocol/governance-contracts@npm:2.6.0" - dependencies: - "@venusprotocol/solidity-utilities": 2.0.0 - hardhat-deploy-ethers: ^0.3.0-beta.13 - module-alias: ^2.2.2 - checksum: 7b4e8034961d02a3ae1f33b9a771e98d1ae39576ae60222433c2d33d184b7f6d8e1368a050dfe1d40f4ce3a0341e6500f00dd6e64c58a35092c942c125247c77 - languageName: node - linkType: hard - -"@venusprotocol/governance-contracts@workspace:.": +"@venusprotocol/governance-contracts@^2.0.0, @venusprotocol/governance-contracts@workspace:.": version: 0.0.0-use.local resolution: "@venusprotocol/governance-contracts@workspace:." dependencies: @@ -3495,7 +3466,7 @@ __metadata: "@nomicfoundation/hardhat-verify": ^2.0.8 "@nomiclabs/hardhat-ethers": ^2.2.1 "@nomiclabs/hardhat-etherscan": ^3.1.2 - "@openzeppelin/contracts": ^4.8.2 + "@openzeppelin/contracts": ^4.9.6 "@openzeppelin/contracts-upgradeable": ^4.8.2 "@openzeppelin/hardhat-upgrades": ^1.22.1 "@semantic-release/changelog": ^6.0.2 @@ -3551,6 +3522,17 @@ __metadata: languageName: unknown linkType: soft +"@venusprotocol/governance-contracts@npm:^1.4.0": + version: 1.4.0 + resolution: "@venusprotocol/governance-contracts@npm:1.4.0" + dependencies: + "@venusprotocol/solidity-utilities": ^1.1.0 + hardhat-deploy-ethers: ^0.3.0-beta.13 + module-alias: ^2.2.2 + checksum: 85c6b6a815edb0befa4c38e3652a58464827d390620210b99575c16960ee6505e95e7c2192ebc972da7ed758d3c62e150d32fbdd1f01acab1731f29b11d1884e + languageName: node + linkType: hard + "@venusprotocol/protocol-reserve@npm:^1.4.0": version: 1.5.0 resolution: "@venusprotocol/protocol-reserve@npm:1.5.0"