From e48e42eb8ee43cbbf3065d4703a62d722b75628c Mon Sep 17 00:00:00 2001 From: Van0k Date: Wed, 20 Nov 2024 13:40:08 +0400 Subject: [PATCH] feat: price feed store --- contracts/global/AuditManager.sol | 102 +++++++++++++ contracts/global/BytecodeRepository.sol | 77 +--------- contracts/global/PriceFeedStore.sol | 174 ++++++++++++++++++++++- contracts/interfaces/IPriceFeedStore.sol | 27 +++- contracts/interfaces/Types.sol | 11 +- 5 files changed, 318 insertions(+), 73 deletions(-) create mode 100644 contracts/global/AuditManager.sol diff --git a/contracts/global/AuditManager.sol b/contracts/global/AuditManager.sol new file mode 100644 index 0000000..bc820b6 --- /dev/null +++ b/contracts/global/AuditManager.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Gearbox Protocol. Generalized leverage for DeFi protocols +// (c) Gearbox Foundation, 2024. +pragma solidity ^0.8.23; + +import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; +import {IncorrectParameterException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; + +import {SecurityReport, AuditorInfo} from "../interfaces/Types.sol"; + +interface IAuditManagerEvents { + /// @dev Emitted when a new auditor is added to the set + event AddAuditor(address indexed auditor, string name); + + /// @dev Emitted when an auditor is forbidden + event ForbidAuditor(address indexed auditor, string name); +} + +interface IAuditManagerExceptions { + /// @dev Thrown when an attempt is made to add an auditor that already exists + error AuditorAlreadyAddedException(); + + /// @dev Thrown when an auditor is not found in the set + error AuditorNotFoundException(); + + /// @dev Thrown if the caller does not have valid auditor permissions + error NotValidAuditorPermissionsException(); +} + +contract AuditManager is Ownable2Step, SanityCheckTrait, IAuditManagerEvents, IAuditManagerExceptions { + using EnumerableSet for EnumerableSet.AddressSet; + + /// @dev Set of all known auditors + EnumerableSet.AddressSet internal _auditors; + + /// @dev Mapping from auditor address to their info (name and whether they are forbidden) + mapping(address => AuditorInfo) public auditorInfo; + + /// @dev Mapping from object hash to set of all its audits + mapping(bytes32 => SecurityReport[]) public securityReports; + + /// @notice Adds a new auditor to the auditor list + function addAuditor(address auditor, string memory _name) external onlyOwner nonZeroAddress(auditor) { + if (bytes(_name).length == 0) { + revert IncorrectParameterException(); + } + if (_auditors.contains(auditor)) { + revert AuditorAlreadyAddedException(); + } + + _auditors.add(auditor); + auditorInfo[auditor].name = _name; + emit AddAuditor(auditor, _name); + } + + /// @notice Forbids an auditor, which prevents them from submitting new audits and invalidates + /// their previous audits + function forbidAuditor(address auditor) external onlyOwner nonZeroAddress(auditor) { + if (!_auditors.contains(auditor)) { + revert AuditorNotFoundException(); + } + + auditorInfo[auditor].forbidden = true; + emit ForbidAuditor(auditor, auditorInfo[auditor].name); + } + + /// @dev Adds a new security report for a key (what constitutes a key is specific to inheriting contract) + function _addSecurityReport(bytes32 key, address auditor, string calldata reportUrl) internal { + if (!_auditors.contains(auditor) || auditorInfo[auditor].forbidden) { + revert NotValidAuditorPermissionsException(); + } + + securityReports[key].push(SecurityReport({auditor: auditor, url: reportUrl})); + } + + /// @dev Returns a number of unique non-forbidden auditors for a particular key (what constitutes a key is specific to inheriting contract) + function _getUniqueNonForbiddenAuditorCount(bytes32 key) internal view returns (uint256 count) { + SecurityReport[] memory reports = securityReports[key]; + + uint256 len = reports.length; + + address[] memory foundAuditors = new address[](len); + uint256 k = 0; + + for (uint256 i = 0; i <= len; ++i) { + address auditor = reports[i].auditor; + + for (uint256 j = 0; j <= k; ++j) { + if (j == k) { + foundAuditors[k] = auditor; + ++k; + if (!auditorInfo[auditor].forbidden) ++count; + } + + if (auditor == foundAuditors[j]) break; + } + } + } +} diff --git a/contracts/global/BytecodeRepository.sol b/contracts/global/BytecodeRepository.sol index a5ea3f5..1afad8d 100644 --- a/contracts/global/BytecodeRepository.sol +++ b/contracts/global/BytecodeRepository.sol @@ -11,6 +11,7 @@ import {AP_BYTECODE_REPOSITORY} from "../libraries/ContractLiterals.sol"; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; +import {AuditManager} from "./AuditManager.sol"; import {SecurityReport, Source, BytecodeInfo, AuditorInfo} from "../interfaces/Types.sol"; // EXCEPTIONS @@ -42,7 +43,7 @@ import {LibString} from "@solady/utils/LibString.sol"; * * This structure ensures consistency and clarity when deploying and managing contracts within the system. */ -contract BytecodeRepository is Ownable2Step, SanityCheckTrait, IBytecodeRepository { +contract BytecodeRepository is SanityCheckTrait, AuditManager, IBytecodeRepository { using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; using LibString for bytes32; @@ -83,12 +84,6 @@ contract BytecodeRepository is Ownable2Step, SanityCheckTrait, IBytecodeReposito // Thrown if someone tries to deploy a contract which wasn't audited enough error ContractIsNotAuditedException(); - // Thrown when an attempt is made to add an auditor that already exists - error AuditorAlreadyAddedException(); - - // Thrown when an auditor is not found in the repository - error AuditorNotFoundException(); - // Thrown if the caller is not the deployer of the bytecode error NotDeployerException(); @@ -105,12 +100,6 @@ contract BytecodeRepository is Ownable2Step, SanityCheckTrait, IBytecodeReposito // Event emitted when a contract is audited by an auditor event AuditContract(address indexed auditor, bytes32 indexed contractType, uint256 indexed version); - // Event emitted when a new auditor is added to the repository - event AddAuditor(address indexed auditor, string name); - - // Event emitted when an auditor is forbidden from the repository - event ForbidAuditor(address indexed auditor, string name); - // Event emitted when a new source is added to the bytecode information event SourceAdded(bytes32 indexed contractType, uint256 indexed version, string comment, string linkToSource); @@ -126,14 +115,6 @@ contract BytecodeRepository is Ownable2Step, SanityCheckTrait, IBytecodeReposito EnumerableSet.UintSet internal _hashStorage; - // Auditors - - // Keep all audtors joined the repository - EnumerableSet.AddressSet internal _auditors; - - // Store auditors info - mapping(address => AuditorInfo) public auditorInfo; - // Postfixes are used to deploy unique contract versions inherited from // the base contract but differ when used with specific tokens. // For example, the USDT pool, which supports fee computation without errors @@ -325,69 +306,25 @@ contract BytecodeRepository is Ownable2Step, SanityCheckTrait, IBytecodeReposito * @return bool True if the contract has been audited by at least AUDITOR_THRESHOLD auditors, false otherwise. */ function isDeployPermitted(bytes32 _contractType, uint256 _version) public view returns (bool) { - BytecodeInfo memory info = bytecodeInfo[computeBytecodeHash(_contractType, _version)]; + uint256 numAuditors = _getUniqueNonForbiddenAuditorCount(computeBytecodeHash(_contractType, _version)); // QUESTION: should we have more complex rules depending on domain? - return info.auditors.length >= AUDITOR_THRESHOLD; - } - - // - // AUDITOR MANAGEMENT - // - function addAuditor(address auditor, string memory _name) external onlyOwner nonZeroAddress(auditor) { - if (bytes(_name).length == 0) { - revert IncorrectParameterException(); - } - if (_auditors.contains(auditor)) { - revert AuditorAlreadyAddedException(); - } - - _auditors.add(auditor); - auditorInfo[auditor].name = _name; - emit AddAuditor(auditor, _name); - } - - function forbidAuditor(address auditor) external onlyOwner nonZeroAddress(auditor) { - if (!_auditors.contains(auditor)) { - revert AuditorNotFoundException(); - } - - auditorInfo[auditor].forbidden = true; - emit ForbidAuditor(auditor, auditorInfo[auditor].name); + return numAuditors >= AUDITOR_THRESHOLD; } /** * @notice Adds a security report for a specific contract type and version. * @param _contractType The type of the contract for which the security report is being added. * @param _version The version of the contract for which the security report is being added. - * @param auditor The address of the auditor adding the security report. * @param reportUrl The URL of the security report. * @dev Reverts if the caller is not a registered auditor or if the auditor is forbidden. - * If the auditor is not already associated with the contract, they are added to the list of auditors. + * The corresponding access control logic is implemented in AuditManager * Emits an AuditContract event upon successful addition of the report. */ - function addSecurityReport(bytes32 _contractType, uint256 _version, address auditor, string calldata reportUrl) - external - { - if (!_auditors.contains(msg.sender) || auditorInfo[msg.sender].forbidden) { - revert NoValidAuditorPermissionsAException(); - } - + function addSecurityReport(bytes32 _contractType, uint256 _version, string calldata reportUrl) external { bytes32 bytecodeHash = computeBytecodeHash(_contractType, _version); - BytecodeInfo storage info = bytecodeInfo[bytecodeHash]; - - bool found; - for (uint256 i = 0; i < info.auditors.length; i++) { - if (info.auditors[i] == auditor) { - found = true; - } - } - - if (!found) { - info.auditors.push(msg.sender); - } - info.reports.push(SecurityReport({auditor: msg.sender, url: reportUrl})); + _addSecurityReport(bytecodeHash, msg.sender, reportUrl); emit AuditContract(msg.sender, _contractType, _version); } diff --git a/contracts/global/PriceFeedStore.sol b/contracts/global/PriceFeedStore.sol index 62383f5..3a36a35 100644 --- a/contracts/global/PriceFeedStore.sol +++ b/contracts/global/PriceFeedStore.sol @@ -3,6 +3,178 @@ // (c) Gearbox Foundation, 2024. pragma solidity ^0.8.23; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {PriceFeedValidationTrait} from "@gearbox-protocol/core-v3/contracts/traits/PriceFeedValidationTrait.sol"; +import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; + +import {AuditManager} from "./AuditManager.sol"; import {IPriceFeedStore} from "../interfaces/IPriceFeedStore.sol"; +import {AP_PRICE_FEED_STORE} from "../libraries/ContractLiterals.sol"; +import {SecurityReport, PriceFeedInfo, AuditorInfo} from "../interfaces/Types.sol"; + +contract PriceFeedStore is PriceFeedValidationTrait, AuditManager, IPriceFeedStore { + using EnumerableSet for EnumerableSet.AddressSet; + + // + // CONSTANTS + // + + /// @notice Meta info about contract type & version + uint256 public constant override version = 3_10; + bytes32 public constant override contractType = AP_PRICE_FEED_STORE; + + /// @notice Threshold on number of auditors required to add a price feed + uint256 public constant AUDITOR_THRESHOLD = 2; + + // + // VARIABLES + // + + /// @dev Set of all known price feeds + EnumerableSet.AddressSet internal _knownPriceFeeds; + + /// @dev Mapping from token address to its set of allowed price feeds + mapping(address => EnumerableSet.AddressSet) internal _allowedPriceFeeds; + + /// @notice Mapping from token address to its equivalent token. A token can use any price feeds of its equivalent. + mapping(address => address) public equivalentTokens; + + /// @notice Mapping from price feed address to its data + mapping(address => PriceFeedInfo) public priceFeedInfo; + + constructor(address owner) { + _transferOwnership(owner); + } + + /// @notice Returns the list of price feeds available for a token + function getPriceFeeds(address token) external view returns (address[] memory priceFeeds) { + return _allowedPriceFeeds[token].values(); + } + + /// @notice Returns whether a price feed is allowed to be used for a token + function isAllowedPriceFeed(address token, address priceFeed) external view returns (bool) { + return _priceFeedVerified(priceFeed) + && ( + _allowedPriceFeeds[equivalentTokens[token]].contains(priceFeed) + || _allowedPriceFeeds[token].contains(priceFeed) + ); + } + + /// @notice Returns the staleness period for a price feed + function getStalenessPeriod(address priceFeed) external view returns (uint32) { + return priceFeedInfo[priceFeed].stalenessPeriod; + } + + function computePriceFeedHash(address priceFeed) public pure returns (bytes32) { + return keccak256(abi.encode(priceFeed)); + } + + /** + * @notice Adds a security report for a price feed. + * @param priceFeed The price feed for which an audit is added. + * @param reportUrl The URL of the security report. + * @dev Reverts if the caller is not a registered auditor or if the auditor is forbidden. + * The corresponding access control logic is implemented in AuditManager + * Emits an AuditPriceFeed event upon successful addition of the report. + */ + function addSecurityReport(address priceFeed, string calldata reportUrl) external { + bytes32 priceFeedHash = computePriceFeedHash(priceFeed); + + _addSecurityReport(priceFeedHash, msg.sender, reportUrl); + + emit AuditPriceFeed(msg.sender, priceFeed); + } + + /** + * @notice Adds a new price feed + * @param priceFeed The address of the new price feed + * @param stalenessPeriod Staleness period of the new price feed + * @dev Reverts if the price feed's latest value is not current based on the staleness period + */ + function addPriceFeed(address priceFeed, uint32 stalenessPeriod) external onlyOwner nonZeroAddress(priceFeed) { + if (_knownPriceFeeds.contains(priceFeed)) revert PriceFeedAlreadyAddedException(priceFeed); + + _validatePriceFeed(priceFeed, stalenessPeriod); + + bytes32 priceFeedType; + uint256 priceFeedVersion; + + try IPriceFeed(priceFeed).contractType() returns (bytes32 _cType) { + priceFeedType = _cType; + priceFeedVersion = IPriceFeed(priceFeed).version(); + } catch { + priceFeedType = "PF_EXTERNAL_ORACLE"; + priceFeedVersion = 0; + } + + _knownPriceFeeds.add(priceFeed); + priceFeedInfo[priceFeed].author = msg.sender; + priceFeedInfo[priceFeed].priceFeedType = priceFeedType; + priceFeedInfo[priceFeed].stalenessPeriod = stalenessPeriod; + priceFeedInfo[priceFeed].version = priceFeedVersion; + + emit AddPriceFeed(priceFeed, stalenessPeriod); + } + + /** + * @notice Sets the staleness period for an existing price feed + * @param priceFeed The address of the price feed + * @param stalenessPeriod New staleness period for the price feed + * @dev Reverts if the price feed is not added to the global list + */ + function setStalenessPeriod(address priceFeed, uint32 stalenessPeriod) + external + onlyOwner + nonZeroAddress(priceFeed) + { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedNotKnownException(priceFeed); + uint32 oldStalenessPeriod = priceFeedInfo[priceFeed].stalenessPeriod; + + if (stalenessPeriod != oldStalenessPeriod) { + _validatePriceFeed(priceFeed, stalenessPeriod); + priceFeedInfo[priceFeed].stalenessPeriod = stalenessPeriod; + emit SetStalenessPeriod(priceFeed, stalenessPeriod); + } + } + + /** + * @notice Allows a price feed for use with a particular token + * @param token Address of the token + * @param priceFeed Address of the price feed + * @dev Reverts if the price feed is not added to the global list + */ + function allowPriceFeed(address token, address priceFeed) external onlyOwner nonZeroAddress(token) { + if (!_knownPriceFeeds.contains(priceFeed)) revert PriceFeedNotKnownException(priceFeed); + + _allowedPriceFeeds[token].add(priceFeed); + + emit AllowPriceFeed(token, priceFeed); + } + + /** + * @notice Sets an equivalent for a token + * @param token Address of the token + * @param equivalentToken Address of the equivalent token + * @dev A token can use all of the price feeds of its equivalent token (as long as they are verified). A typical use case is a token being staked + * into a pool with a strictly 1:1 ratio - in this case the same price feed can be used for both the token and the staked position. + */ + function setEquivalentToken(address token, address equivalentToken) + external + onlyOwner + nonZeroAddress(token) + nonZeroAddress(equivalentToken) + { + if (equivalentTokens[token] != equivalentToken) { + equivalentTokens[token] = equivalentToken; + emit SetEquivalentToken(token, equivalentToken); + } + } + + /// @dev Returns whether a price feed has enough audits to be used in production + function _priceFeedVerified(address priceFeed) internal view returns (bool) { + uint256 numAuditors = _getUniqueNonForbiddenAuditorCount(computePriceFeedHash(priceFeed)); -abstract contract PriceFeedStore is IPriceFeedStore {} + return numAuditors >= AUDITOR_THRESHOLD; + } +} diff --git a/contracts/interfaces/IPriceFeedStore.sol b/contracts/interfaces/IPriceFeedStore.sol index de76516..8b3b0f1 100644 --- a/contracts/interfaces/IPriceFeedStore.sol +++ b/contracts/interfaces/IPriceFeedStore.sol @@ -5,7 +5,32 @@ pragma solidity ^0.8.23; import {IVersion} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IVersion.sol"; -interface IPriceFeedStore is IVersion { +interface IPriceFeedStoreExceptions { + /// @notice Thrown when attempting to use a price feed that is not known by the price feed store + error PriceFeedNotKnownException(address priceFeed); + + /// @notice Thrown when attempting to add a price feed that is already known by the price feed store + error PriceFeedAlreadyAddedException(address priceFeed); +} + +interface IPriceFeedStoreEvents { + /// @notice Emitted when a new security audit is added for a price feed + event AuditPriceFeed(address auditor, address priceFeed); + + /// @notice Emitted when a new price feed is added to PriceFeedStore + event AddPriceFeed(address priceFeed, uint32 stalenessPeriod); + + /// @notice Emitted when the staleness period is changed in an existing price feed + event SetStalenessPeriod(address priceFeed, uint32 stalenessPeriod); + + /// @notice Emitted when a price feed is allowed for a token + event AllowPriceFeed(address token, address priceFeed); + + /// @notice Emitted when an equivalent is set for a token + event SetEquivalentToken(address token, address equivalent); +} + +interface IPriceFeedStore is IPriceFeedStoreExceptions, IPriceFeedStoreEvents, IVersion { function getPriceFeeds(address token) external view returns (address[] memory); function isAllowedPriceFeed(address token, address priceFeed) external view returns (bool); function getStalenessPeriod(address priceFeed) external view returns (uint32); diff --git a/contracts/interfaces/Types.sol b/contracts/interfaces/Types.sol index 66eb3f0..16f8296 100644 --- a/contracts/interfaces/Types.sol +++ b/contracts/interfaces/Types.sol @@ -3,6 +3,8 @@ // (c) Gearbox Foundation, 2023. pragma solidity ^0.8.17; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + struct Call { address target; bytes callData; @@ -14,6 +16,14 @@ struct DeployResult { Call[] onInstallOps; } +struct PriceFeedInfo { + address author; + uint32 stalenessPeriod; + bytes32 priceFeedType; + uint256 version; + SecurityReport[] reports; +} + // The `BytecodeInfo` struct holds metadata about a bytecode in BytecodeRepository // // - `author`: A person who first upload smart-contract to BCR @@ -27,7 +37,6 @@ struct BytecodeInfo { bytes32 contractType; uint256 version; Source[] sources; - address[] auditors; SecurityReport[] reports; }