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
BoundValidator
contract 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
BoundValidator
against 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
BoundValidator
says "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
TwapOracle
says that thebaseUnit
signifies the "decimals of the underlying asset", but it actually takes the value of1e{decimals}
of the asset. - Line 25 of
TwapOracle
says 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.sol
could be namedIVBep20.sol
. -
In
ResilientOracle
:- The
struct
fieldenableFlagsForOracles
could be renamed toisEnabled
. - The function
enableOracle
could be changed toupdateOracleEnablement
to reflect both the enablement and disablement of the oracle. - The function
updatePrice
could be changed toupdatePivotPrice
to provide clarity as to which oracle is being updated. - The state variable
tokenConfigs
could be changed to_tokenConfigs
since it is aprivate
variable.
- The
-
In
BinanceOracle
:- The variable
decimalDelta
could be renamed todecimals
to avoid confusion with other parts of the codebase, asdecimalDelta
typically refers to the value18 - decimals
. - The function
compare
could be renamed to_compare
since it is aninternal
function.
- The variable
-
In
BoundValidator
:- The struct
ValidateConfig
could be renamed toValidatorConfig
orBoundValidatorConfig
for consistency.
- The struct
-
In
TwapOracle
:- The
struct
fieldisBnbBased
could be renamed toisWbnbBased
. - The function
pokeWindowValues
could be renamed to_pokeWindowValues
since it is aninternal
function.
- 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
,_getFallbackOraclePrice
and_getUnderlyingAsset
- In
BinanceOracle
, in the functioncompare
- In
BoundValidator
, in the functions_isWithinAnchor
and_getUnderlyingAsset
- In
ChainlinkOracle
, in the functions_getUnderlyingPriceInternal
and_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 functionsinitialize
andgetUnderlyingPrice
- In
BoundValidator
, in the functionsinitialize
andvalidatePriceWithAnchorPrice
- In
ChainlinkOracle
, in the functionsinitialize
andgetUnderlyingPrice
- In
PythOracle
, in the functionsinitialize
andgetUnderlyingPrice
- In
TwapOracle
, in the functionsinitialize
andupdateTwap
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
ResilientOracle
contract, the_getFallbackOraclePrice
and_getMainOraclePrice
functions 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
_getUnderlyingAsset
function is very similar or the same in many contracts, such asResilientOracle
andBoundValidator
. Consider moving this functionality to a separate library. - Similarly, the
setTokenConfigs
andsetTokenConfig
functions in theResilientOracle
,ChainlinkOracle
,TwapOracle
, andPythOracle
are practically the same and can be moved to a library. - The
setDirectPrice
andsetUnderlyingPrice
functions in theChainlinkOracle
contract share almost all their functionality. ThesetDirectPrice
function could be called from thesetUnderlyingPrice
function after looking up theasset
address from thevToken
address, 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
PythOracleSet
event in thePythOracle
contract should emit both the old and new oracle addresses. In theinitializer
function, it should emit(0, underlyintPythOracle_)
and in thesetUnderlyingPythOracle
function it should emit (old, new), similar to thePricePosted
event of theChainlinkOracle
contract, which emits both the old and new oracle price. - The
BatchPriceFeedUpdate
event inPythInterface
does not have any parameters indexed. ThechainId
field 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
setOracle
function of theResilientOracle
contract, it is advisable to check that for non-MAIN
oracles (i.e.,PIVOT
andFALLBACK
), the specified address is different from theMAIN
oracle address. - In the
setTokenConfig
function of theResilientOracle
contract, 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 thetokenConfigs
is a zero address. By implementing this validation, updating the oracle for a specific token would only be possible through thesetOracle
andenableOracle
functions, 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
1e18
is 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.