- June 13, 2023
OpenZeppelin Security
OpenZeppelin Security
Security Audits
June 13, 2023
This security assessment was prepared by OpenZeppelin.
Table of Contents
- Table of Contents
- Summary
- Scope
- System Overview
- Security Model and Trust Assumptions
- Low Severity
- Notes & Additional Information
- Constants Not Using UPPER_CASE Format
- Lack of Idiomatic Code Layout
- Superfluous Casting
- Global Namespace Pollution
- Naming Suggestions
- Ungraceful Handling of Function Call
- Extraneous virtual and Visibility Keywords
- Multiple Contract Declarations per File
- Code Redundancy
- Duplicated Event Parameter
- Incomplete Event Emissions
- Lack of Input Validation
- Magic Constant
- Unnecessary Repeated Fields in Structs
- Unnecessary Variable Initialization in initialize
- Conclusions
- Appendix
Summary
- Type
- DeFi
- Timeline
- From 2023-05-08
- To 2023-05-23
- Languages
- Solidity
- Total Issues
- 17 (8 resolved, 2 partially resolved)
- Critical Severity Issues
- 0 (0 resolved)
- High Severity Issues
- 0 (0 resolved)
- Medium Severity Issues
- 0 (0 resolved)
- Low Severity Issues
- 2 (2 resolved)
- Notes & Additional Information
- 15 (6 resolved, 2 partially resolved)
Scope
We audited the VenusProtocol/oracle repository at the 78b1a41 commit.
contracts
├── interfaces
│ ├── FeedRegistryInterface.sol
│ ├── OracleInterface.sol
│ ├── PublicResolverInterface.sol
│ ├── PythInterface.sol
│ ├── SIDRegistryInterface.sol
│ └── VBep20Interface.sol
├── libraries
│ └── PancakeLibrary.sol
├── oracles
│ ├── BinanceOracle.sol
│ ├── BoundValidator.sol
│ ├── ChainlinkOracle.sol
│ ├── PythOracle.sol
│ └── TwapOracle.sol
└── ResilientOracle.sol
System Overview
The Venus protocol is a lending and borrowing solution on the Binance Smart Chain. As part of this protocol, oracles are required to obtain current fair market prices for certain assets. These oracles will be used for pricing core and isolated pools on Venus. The tokens that these oracles will be used for will be determined through governance. Any tokens that have more than 18 decimals, as well as fee-on-transfer and ERC-777 tokens will not be allowed.
Rather than relying on one single data feed, Venus has a ResilientOracle that obtains prices from various oracles, analyzes if these prices are trustworthy using the BoundValidator contract, and returns the resulting price. The specifics of how the ResilientOracle works are detailed below. The various other oracles used by this ResilientOracle are the BinanceOracle, ChainlinkOracle, PythOracle, and TwapOracle. Each of these oracle contracts designed by Venus calls their corresponding data feeds to obtain prices (e.g., ChainlinkOracle calls the oracle contract from Chainlink), and the TwapOracle uses the pool data from PancakeSwap.
The ResilientOracle, BinanceOracle, BoundValidator, ChainlinkOracle, PythOracle, and TwapOracle contracts are all meant to be used as implementation contracts and sit behind transparent proxies. This allows for upgradeability in the future. However, there are no plans to build more contracts that inherit from any of these oracles, nor the BoundValidator. Each of these contracts has a state variable named _accessControlManager, in which there is an address with default admin privileges that can determine which addresses are allowed to call the different functions in these oracles. In addition, all of these contracts inherit from OpenZeppelin's Ownable2Step contract, and the owner has the power to change the access control manager.
Design
As mentioned, the ResilientOracle contract obtains prices from other oracles and compares them to check whether a price is within certain boundaries, finally deciding whether the price is valid or not. These oracles are categorized as MAIN, PIVOT, and FALLBACK. For someone to get a price of a listed asset, the getUnderlyingPrice function must be called, which works as follows:
- If the MAIN oracle is set, enabled, and returns a price that is not 0, and the PIVOT oracle is not enabled, then the MAIN oracle price is retrieved without checking it against any other oracle.
- If the MAIN oracle is set, enabled, and returns a price that is not 0, and the PIVOT price is enabled but returns an invalid price, then it checks that the MAIN price is within the boundaries defined in the
BoundValidatorcontract with the FALLBACK price. If it is, it returns the MAIN price. Otherwise, it reverts. - If the MAIN oracle is set, enabled, and returns a price that is not 0, and the PIVOT price is enabled and is not 0, it checks that the MAIN price is within the boundaries defined in the
BoundValidatoragainst the PIVOT price. - If it is, it returns the MAIN oracle price.
- Otherwise, it checks that the FALLBACK price is within the boundaries of the PIVOT price.
- If it is, it returns the FALLBACK price.
- Otherwise, it checks that the MAIN price is within the boundaries of the FALLBACK price.
- If it is, it returns the MAIN price.
- Otherwise, it reverts.
- If the MAIN oracle is not set and the PIVOT price is set and is not 0, and the FALLBACK is set, is not 0, and is within the boundaries of the PIVOT price, then it returns the FALLBACK price. Otherwise, it reverts.
The MAIN, PIVOT, and FALLBACK oracles could be any of the oracle contracts mentioned above. Nevertheless, the ResilientOracle defines the updateOracle function, which assumes that the PIVOT contract will always be the TwapOracle by default.
Security Model and Trust Assumptions
The owner is a single address that can perform the critical administrative action of setting the access control manager. According to Venus, the owner is a time-locked contract that is used through the governance process. The owner is considered a trusted entity.
The access control manager is a contract, in which there is an address with default admin privileges that can perform the critical administrative action of giving and revoking roles for different addresses. These roles provide permissions for addresses to call privileged functions within the different oracle contracts as well as the BoundValidator. These functions include setting and enabling the oracle addresses, setting bounds configurations, adding or removing tokens from the oracles, setting data feed configurations, and even pausing and unpausing the contract. According to Venus, the address with the default admin privileges for the access control manager is a time-locked contract that is used through the governance process. Currently, this address is the same one as the owner.
The data feeds of Chainlink, Binance, and Pyth that are relied upon by the oracles are considered trusted. While ultimately the ResilientOracle returns the price to the user, if a malicious adversary were to be able to control one or more of these data feeds, they could implement a denial-of-service attack, or even worse, provide incorrect data to the system. The security of the system relies on the difficulty of taking control of one or more of these relatively centralized sources.
In addition, Venus will rely on PancakeSwap pools to provide the data feed for the TwapOracle. This source of data is decentralized and thus is more vulnerable to price manipulation, hence the requirement for TWAP. The difficulty of price manipulations for these pools is directly related to their liquidity. Therefore, the security of this oracle model is dependent on using tokens that are more popular and have a larger pool depth. With the decline in popularity of PancakeSwap V2 and the migration to V3, it will be important for the protocol to closely monitor the tokens of interest.
Lastly, it is worth noting that all the mentioned oracle contracts feature a getUnderlyingPrice function. However, regardless of the number of decimals associated with the underlying token or the vToken address, these functions consistently return prices with 36 decimals minus the number of decimals of the underlying token. This applies to the ResilientOracle as well, which serves as the primary point of interaction for other contracts. It is assumed that this consideration is taken into account throughout the out-of-scope codebase when invoking the getUnderlyingPrice functions and performing calculations based on the returned results.
Low Severity
Outdated Chainlink Interface
In line 201 of ChainlinkOracle, the interface AggregatorV2V3Interface is used, which inherits from both the AggregatorInterface and the AggregatorV3Interface. Chainlink recommends using the AggregatorV3Interface, as shown in its documentation. This prevents the usage of deprecated functions in the AggregatorInterface, which do not throw errors if no answer has been reached, but instead return 0. The dependence on this unexpected behavior increases the attack surface for the calling contract.
Consider updating the interface used in the ChainlinkOracle from AggregatorV2V3Interface to AggregatorV3Interface.
Update: Resolved in pull request #84 at commit ddd4b02.
Misleading Documentation
The following documentation is misleading:
- Line 83 of
BoundValidatorsays "range error thrown if lower bound is greater than upper bound" instead of "range error thrown if lower bound is greater than or equal to upper bound". - Line 18 of
TwapOraclesays that thebaseUnitsignifies the "decimals of the underlying asset", but it actually takes the value of1e{decimals}of the asset. - Line 25 of
TwapOraclesays that "XVS-WBNB is not reversed, while WBNB-XVS is", when in reality the opposite is true.
Consider updating these misleading comments to match the code's intention.
Update: Resolved in pull request #84 at commit f4352f1.
Notes & Additional Information
Constants Not Using UPPER_CASE Format
In TwapOracle.sol the following constants are not using UPPER_CASE format:
According to the Solidity Style Guide, constants should be named with all capital letters with underscores separating words. For better readability, consider following this convention.
Update: Resolved in pull request #84 at commit 70a2211.
Lack of Idiomatic Code Layout
According to the Solidity Style Guide, elements inside of contracts, libraries, and interfaces should be in the following order: type declarations, state variables, events, errors, modifiers, and finally functions.
In addition, the guide states that "functions should be grouped according to their visibility and ordered" first by the constructor, followed by the receive function (if it exists), the fallback function (if it exists), public functions, internal functions, and finally private functions. In addition, since the contracts are implementation contracts, it is idiomatic to place the initialize function directly below the constructor.
Throughout the codebase, the layouts of the files do not follow the Solidity Style Guide and are not consistent with each other. Consider moving the modifiers, events, errors, and functions in these files to match the Solidity Style Guide.
Update: Acknowledged, not resolved. The Venus team stated:
Ignoring the Pyth Interface because it was copied from the original project and linting would make the diff more complicated when updating.
Superfluous Casting
In line 305 and line 307 of TwapOracle, the parameter vToken, which is already of type address, is unnecessarily cast with the address keyword.
In addition, in lines 182 and 207, the value 18 is unnecessarily cast with the uint256 keyword.
Consider removing the superfluous casting in order to improve the readability of the codebase.
Update: Resolved in pull request #84 at commit 66707bd.
Global Namespace Pollution
In BoundValidator, ChainlinkOracle, PythOracle, and TwapOracle, there are structs defined outside of the contracts and therefore in the global namespace. This pattern is used when there are multiple contract implementations within one source file that need to reference the same structs. However, in these files in the codebase, there is only one implementation contract per file, so this methodology is unnecessary.
Consider either placing these structs inside the contracts' definitions or within the corresponding interfaces to reduce global namespace pollution.
Update: Resolved in pull request #84 at commit 28f4924.
Naming Suggestions
To favor explicitness and readability, the following locations in the contracts may benefit from better naming:
-
The interface file names as well as the interfaces themselves could take the format of
I{...}as opposed to{...}Interface. For example,VBep20Interface.solcould be namedIVBep20.sol. -
In
ResilientOracle:- The
structfieldenableFlagsForOraclescould be renamed toisEnabled. - The function
enableOraclecould be changed toupdateOracleEnablementto reflect both the enablement and disablement of the oracle. - The function
updatePricecould be changed toupdatePivotPriceto provide clarity as to which oracle is being updated. - The state variable
tokenConfigscould be changed to_tokenConfigssince it is aprivatevariable.
- The
-
In
BinanceOracle:- The variable
decimalDeltacould be renamed todecimalsto avoid confusion with other parts of the codebase, asdecimalDeltatypically refers to the value18 - decimals. - The function
comparecould be renamed to_comparesince it is aninternalfunction.
- The variable
-
In
BoundValidator:- The struct
ValidateConfigcould be renamed toValidatorConfigorBoundValidatorConfigfor consistency.
- The struct
-
In
TwapOracle:- The
structfieldisBnbBasedcould be renamed toisWbnbBased. - The function
pokeWindowValuescould be renamed to_pokeWindowValuessince it is aninternalfunction.
- The
Consider renaming as suggested above to improve the consistency and readability of the codebase.
Update: Acknowledged, not resolved. The Venus team stated:
We prefer to reduce the number of changes.
Ungraceful Handling of Function Call
When the function _getUnderlyingAsset in ResilientOracle is called, if the passed vToken does not equal that of vBnb nor vai, it calls the function VBep20Interface(vToken).underlying(). If the address passed in asvToken does not have the underlying method, it does not fail gracefully with an error message.
Similarly, there are instances of ungraceful handling in getUnderlyingPrice in BinanceOracle, _getUnderlyingAsset in BoundValidator, setUnderlyingPrice and _getUnderlyingPriceInternal in ChainlinkOracle, getUnderlyingPrice in PythOracle, and _getUnderlyingAsset in TwapOracle.
Consider handling these cases gracefully using try-catch and returning descriptive error messages.
Update: Acknowledged, not resolved. The Venus team stated:
We prefer to reduce the number of changes.
Extraneous virtual and Visibility Keywords
Throughout the codebase, there are extraneous virtual and visibility keywords. From what is understood, there are no plans to inherit from existing contracts. Thus, it is best practice to limit the scope to what is designed, thereby removing extraneous virtual keywords and using more restrictive visibility keywords.
Places with unnecessary virtual keywords:
- In
BoundValidator, in the functionssetValidateConfigs,setValidateConfig, andvalidatePriceWithAnchorPrice - In
TwapOracle, in the function_updateTwapInternal
Places with unnecessary internal keywords (replace with private):
- In
ResilientOracle, in the functions_getMainOraclePrice,_getFallbackOraclePriceand_getUnderlyingAsset - In
BinanceOracle, in the functioncompare - In
BoundValidator, in the functions_isWithinAnchorand_getUnderlyingAsset - In
ChainlinkOracle, in the functions_getUnderlyingPriceInternaland_getChainlinkPrice - In
TwapOracle, in the functions_updateTwapInternal,pokeWindowValues, and_getUnderlyingAsset
Places with unnecessary public keywords (replace with external):
- In
ResilientOracle, in the functioninitialize - In
BinanceOracle, in the functionsinitializeandgetUnderlyingPrice - In
BoundValidator, in the functionsinitializeandvalidatePriceWithAnchorPrice - In
ChainlinkOracle, in the functionsinitializeandgetUnderlyingPrice - In
PythOracle, in the functionsinitializeandgetUnderlyingPrice - In
TwapOracle, in the functionsinitializeandupdateTwap
Consider removing extraneous virtual keywords and using more restrictive visibility keywords following the recommendations above.
Update: Resolved in pull request #84 at commit e11f135.
Multiple Contract Declarations per File
In the file PythInterface, there is a contract PythStructs, an interface IPyth, and an abstract contract AbstractPyth. To improve understandability and readability for developers and reviewers, it is recommended to include one contract or interface per file.
The contract PythStructs contains no functions, and only two structs. Consider moving these structs into PythOracle. Consider removing AbstractPyth completely, since no contracts inherit it.
The file PancakeLibrary also contains more than one library and interface declaration in the same file. Consider separating the interface and libraries into their own files to improve the readability of the codebase.
Update: Acknowledged, not resolved. The Venus team stated:
We prefer to reduce the number of changes.
Code Redundancy
We recommend the following high-level refactors to improve the readability, composability and overall quality of the codebase:
- In the
ResilientOraclecontract, the_getFallbackOraclePriceand_getMainOraclePricefunctions have very similar code. They can be consolidated into one function to prevent changes in two different places, which increases the space for error in future changes to the codebase. - The
_getUnderlyingAssetfunction is very similar or the same in many contracts, such asResilientOracleandBoundValidator. Consider moving this functionality to a separate library. - Similarly, the
setTokenConfigsandsetTokenConfigfunctions in theResilientOracle,ChainlinkOracle,TwapOracle, andPythOracleare practically the same and can be moved to a library. - The
setDirectPriceandsetUnderlyingPricefunctions in theChainlinkOraclecontract share almost all their functionality. ThesetDirectPricefunction could be called from thesetUnderlyingPricefunction after looking up theassetaddress from thevTokenaddress, or alternatively an internal_setPrice(asset)function that is called from both functions could be introduced.
Update: Acknowledged, not resolved. The Venus team stated:
We prefer to reduce the number of changes.
Duplicated Event Parameter
In the ChainlinkOracle contract, the PricePosted event defined has four fields. This event is only used in line 80 and line 95. Since in both cases the last two fields (requestedPriceMantissa and newPriceMantissa) are always equal, consider removing one of them.
Update: Resolved in pull request #84 at commit 5d811fd.
Incomplete Event Emissions
The following suggestions are provided to improve the ability of off-chain services to search and filter for specific events:
- The
PythOracleSetevent in thePythOraclecontract should emit both the old and new oracle addresses. In theinitializerfunction, it should emit(0, underlyintPythOracle_)and in thesetUnderlyingPythOraclefunction it should emit (old, new), similar to thePricePostedevent of theChainlinkOraclecontract, which emits both the old and new oracle price. - The
BatchPriceFeedUpdateevent inPythInterfacedoes not have any parameters indexed. ThechainIdfield could be indexed.
Update: Partially resolved in pull request #84 at commit cf6a66c. Regarding the first bullet point, the Venus team updated the PythOracleSet event to emit both the old and new oracle addresses. Furthermore, the initializer and setUnderlyingPythOracle functions, which emit this event, have been updated correspondingly. Regarding the second bullet point, the Venus team stated:
Ignoring the Pyth Interface because it was copied from the original project and linting would make the diff more complicated when updating.
Lack of Input Validation
We recommend implementing the following input validations:
- In the
setOraclefunction of theResilientOraclecontract, it is advisable to check that for non-MAINoracles (i.e.,PIVOTandFALLBACK), the specified address is different from theMAINoracle address. - In the
setTokenConfigfunction of theResilientOraclecontract, there are currently no checks in place to ensure that a new configuration does not override an existing one. To address this, it is recommended to verify that the existing token asset address in thetokenConfigsis a zero address. By implementing this validation, updating the oracle for a specific token would only be possible through thesetOracleandenableOraclefunctions, reducing the likelihood of errors.
While this issue does not pose a security risk, the absence of validation on user-controlled parameters may lead to erroneous transactions, particularly if some clients default to sending null parameters when none are specified.
Update: Acknowledged, not resolved. The Venus team stated:
These functions will be executed via Governance, with a timelock of 3 days, and the vote of the community, so there should not be errors in the inputs.
Magic Constant
The usage of the constant 100e16 on line 123 of BoundValidator.sol lacks sufficient explanation regarding its origin and purpose.
Developers should define a constant variable for every magic value used (including booleans), giving it a clear and self-explanatory name. Additionally, for complex values, inline comments explaining how they were calculated or why they were chosen are highly recommended.
Consider following the Solidity Style Guide to define the constant in UPPER_CASE_WITH_UNDERSCORES format with the corresponding specific public getter to read it.
Update: Partially resolved in pull request #84 at commit c420a18. The Venus team stated:
Mitigated. We added a comment to explain why the multiplication by the constant
1e18is needed.
Unnecessary Repeated Fields in Structs
The contracts ResilientOracle, TwapOracle, PythOracle, and ChainlinkOracle define a TokenConfig struct. This struct is utilized in the tokenConfigs mappings to track various properties of each token's configuration. However, these mappings already use the asset address as the key for each asset, while also including an asset field within the structs. This repetition results in storing unnecessary information.
A similar pattern can be observed in the ValidateConfig struct within the BoundValidator contract.
To optimize storage and avoid redundant data, it is recommended to remove the asset field from the structs. Instead, consider utilizing an existing field within the struct or introducing a boolean field to check the configuration's validity.
Update: Acknowledged, not resolved. The Venus team stated:
We prefer to reduce the number of changes.
Unnecessary Variable Initialization in initialize
The boundValidator variable in the ResilientOracle contract is currently set in the initialize function. However, since this variable remains unchanged after initialization, it can be optimized by declaring it as an immutable variable in the constructor.
By making boundValidator immutable, initialization inconsistencies can be avoided, and gas costs can be reduced when accessing it. This is especially beneficial when calling the getUnderlyingPrice function, where accessing boundValidator may occur up to three times.
To improve efficiency and maintain consistency, consider declaring boundValidator as an immutable variable in the constructor of the ResilientOracle contract.
Update: Resolved in pull request #84 at commit 4f56661.
Conclusions
An in-depth review of the oracles was performed, and several low-severity issues and notes were identified in the system. The Venus Protocol team provided dedicated documentation, which clarified the system's design. The Venus team was very responsive throughout the audit.
Appendix
Monitoring Recommendations
While audits help in identifying code-level issues in the current implementation and potentially the code deployed in production, the Venus Protocol team is encouraged to consider incorporating monitoring activities in the production environment. Ongoing monitoring of deployed contracts helps identify potential threats and issues affecting production environments. With the goal of providing a complete security assessment, the monitoring recommendations section raises several actions addressing trust assumptions and out-of-scope components that can benefit from on-chain monitoring. Consider monitoring the following items:
- Commonly reverting oracles may indicate a lack of fresh data from their underlying feeds.
- Frequent failing of the bound validator may indicate that the TWAP oracle has low volume or liquidity.
- The amount of liquidity in the pools that are used by the TWAP oracle.
- Any change of ownership or granting of privileged roles.