Prepared by:
HALBORN
Last Updated 09/04/2024
Date of Engagement by: August 1st, 2024 - August 16th, 2024
100% of all REPORTED Findings have been addressed
All findings
4
Critical
1
High
2
Medium
0
Low
1
Informational
0
The Relative Finance team
engaged Halborn to conduct a security assessment on their smart contracts, beginning on August 01, 2024, and ending on August 16, 2024. The security assessment was scoped to the smart contracts inside their rfx-contracts
GitHub repository, located at https://github.com/relative-finance/rfx-contracts. The engagement was around the changes being done in the following pull reques https://github.com/relative-finance/rfx-contracts/pull/41.
The team at Halborn was provided two weeks for the engagement and assigned one full-time security engineer to assess the security of the smart contracts. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this assessment is to achieve the following:
Ensure that the system operates as intended.
Identify potential security issues.
Identify lack of best practices within the codebase.
Identify systematic risks that may pose a threat in future releases.
In summary, Halborn identified some security issues that were successfully addressed by the Relative Finance team
.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:
Research into architecture and purpose.
Smart contract manual code review and walkthrough.
Graphing out functionality and contract logic/connectivity/functions (solgraph
).
Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic-related vulnerability classes.
Manual testing by custom scripts.
Static Analysis of security for scoped contract, and imported functions (slither
).
Testnet deployment (Foundry
).
Critical
1
High
2
Medium
0
Low
1
Informational
0
Impact x Likelihood
HAL-01
HAL-03
HAL-04
HAL-02
Security analysis | Risk level | Remediation Date |
---|---|---|
Mismatch between stored and reported Pyth prices | Critical | Solved - 08/12/2024 |
Missing payable keyword renders some functions useless | High | Solved - 08/12/2024 |
Not sending value renders withOraclePricesForAtomicAction modifier useless | High | Solved - 08/14/2024 |
Pyth oracle price is not validated properly | Low | Solved - 08/12/2024 |
// Critical
When the price of a given asset falls below 1
, Pyth oracle returns a positive price
field BUT a negative expo
. However, the contract PythDataStreamProvider
retrieves such a value from the dataStore
' s storage, being type-casted as an uint256
. That makes the contract handle the price wrongly as for an exponent of -1
, it will treat the price as if it had an exponent of 1
, which is completely erroneous as it does not reflect the actual value of the given asset.
The bug is pretty visual:
uint256 precision = _getDataStreamMultiplier(token);
uint256 adjustedBidPrice = Precision.mulDiv(uint256(minPrice), precision, Precision.FLOAT_PRECISION);
uint256 adjustedAskPrice = Precision.mulDiv(uint256(maxPrice), precision, Precision.FLOAT_PRECISION);
The function being called is defined as:
function _getDataStreamMultiplier(address token) internal view returns (uint256) {
uint256 multiplier = dataStore.getUint(Keys.dataStreamMultiplierKey(token));
if (multiplier == 0) {
revert Errors.EmptyDataStreamMultiplier(token);
}
return multiplier;
}
It can be seen that the possible values for such a multiplier are only positive or zero, and the final price is multiplied by it. That makes it impossible to handle negative exponents that would otherwise return less-than-one prices.
It is recommended to handle prices with negative coefficients by upcasting or downcasting the prices depending on the value of such a coefficient before doing any calculations.
SOLVED: The Relative Finance team solved this issue by updating the given multiplier
depending on the value of the returned expo
field from the Pyth price feed:
report.maxPrice = (r1.maxPrice * (10 ** 18)) / r2.minPrice;
report.minPrice = (r1.minPrice * (10 ** 18)) / r2.maxPrice;
report.expo = r1.expo - r2.expo - 18;
report.publishTime = r1.publishTime > r2.publishTime ? r1.publishTime : r2.publishTime;
}
if (report.minPrice <= 0 || report.maxPrice <= 0) {
revert Errors.InvalidDataStreamPrices(token, int192(report.minPrice), int192(report.maxPrice));
}
uint256 precision = _getDataStreamMultiplier(token);
if (report.expo < 0) {
precision = precision / (10 ** uint32(-1 * report.expo));
} else {
precision = precision * (10 ** uint32(report.expo));
}
uint256 adjustedBidPrice = Precision.mulDiv(uint256(report.minPrice), precision, Precision.FLOAT_PRECISION);
uint256 adjustedAskPrice = Precision.mulDiv(uint256(report.maxPrice), precision, Precision.FLOAT_PRECISION);
// High
Every call to _validatePrices
should come from a payable
function as it sends a fee to the Pyth provider BUT the functions validatePrices
and setPricesForAtomicAction
do not have such a keyword, as oposed to setPrices
, so they render unusable. The place where ETH is needed is here.
The affected functions are:
function validatePrices(
OracleUtils.SetPricesParams memory params,
bool forAtomicAction
) external onlyController returns (OracleUtils.ValidatedPrice[] memory) {
return _validatePrices(params, forAtomicAction);
}
function setPricesForAtomicAction(
OracleUtils.SetPricesParams memory params
) external onlyController {
OracleUtils.ValidatedPrice[] memory prices = _validatePrices(params, true);
_setPrices(prices);
}
it can be seen they do NOT have the payable
keyword, so calls to these functions can NOT carry ETH. However, they both call _validatePrices
, which eventually calls:
OracleUtils.ValidatedPrice memory validatedPrice = IOracleProviderPayable(provider).getOraclePrice{value: fee}(
token,
data
);
but no ETH can be attached, so no fee can be sent to the Pyth provider, which reverts the transaction in those situation, rendering this core functionality useless.
It is recommended to add the payable
keyword to those functions that deal with msg.value
, so that sending them ETH does not revert and can behave correctly.
SOLVED: The Relative Finance team solved this issue by adding the payable
keyword in the affected functions:
function validatePrices(
OracleUtils.SetPricesParams memory params,
bool forAtomicAction
) external payable onlyController returns (OracleUtils.ValidatedPrice[] memory) {
return _validatePrices(params, forAtomicAction);
}
function setPricesForAtomicAction(
OracleUtils.SetPricesParams memory params
) external payable onlyController {
OracleUtils.ValidatedPrice[] memory prices = _validatePrices(params, true);
_setPrices(prices);
}
// High
Inside the OracleModule
contract, withOraclePricesForAtomicAction
modifier, there is a call to setPricesForAtomicAction
inside the Oracle
contract, which calls _validatePrices
and so it sends a given fee to the Pyth provider for its services. However, no ETH is sent (without taking into account the previous issue around the payable
keyword), so the Pyth endpoint will revert the transaction as no fee was provided.
Pretty visual:
modifier withOraclePricesForAtomicAction(
OracleUtils.SetPricesParams memory params
) {
oracle.setPricesForAtomicAction(params);
_;
oracle.clearAllPrices();
}
As seen before, the call to setPricesForAtomicAction
needs some ETH to be used as fees, but no ETH is sent, so any function with this modifier, namely WithdrawalHandler::executeAtomicWithdrawal
renders useless.
It is recommended to send the required fee attached to the call to oracle.setPricesForAtomicAction
, so that the ETH is sent through the contract to the Pyth endpoint.
SOLVED: The Relative Finance team solved this issue by sending the required fee alongside the call to the oracle
:
modifier withOraclePricesForAtomicAction(
OracleUtils.SetPricesParams memory params
) {
oracle.setPricesForAtomicAction{value: msg.value}(params);
_;
oracle.clearAllPrices();
}
// Low
The PythDataStreamProvider
contract does not perform input validation on the price
, conf
, and expo
values returned from the called price feed, which can lead to the contract accepting invalid or untrusted prices. Those values should be checked as clearly stated in the official documentation.
The contract should revert the transaction here if one of the following conditions is triggered:
price <= 0
expo < -18
conf > 0 && (price / int64(conf) < MIN_CONF_RATIO
for a given MIN_CONF_RATIO
SOLVED: The Relative Finance team solved this issue by checking the recommended conditions, and reverting the transaction if the values were not the expected ones.
Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their ABIs and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.
Overall, the reported issues were not mostly low/informational issues that did not pose a real threat to the system, so they were not considered to be part of this report.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed