v2.1 - RFX Exchange


Prepared by:

Halborn Logo

HALBORN

Last Updated 09/04/2024

Date of Engagement: August 1st, 2024 - August 16th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

4

Critical

3

High

0

Medium

1

Low

0

Informational

0


1. Introduction

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.

2. Assessment Summary

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.

3. Test Approach and Methodology

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).

4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

Files and Repository
(a) Repository: rfx-contracts
(b) Assessed Commit ID: feb04bf
(c) Items in scope:
  • ./contracts/adl/AdlUtils.sol
  • ./contracts/config/Config.sol
  • ./contracts/Timelock.sol
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

3

High

0

Medium

1

Low

0

Informational

0

Impact x Likelihood

HAL-01

HAL-02

HAL-03

HAL-04

Security analysisRisk levelRemediation Date
Mismatch between stored and reported Pyth pricesCriticalSolved - 08/12/2024
Missing payable keyword renders some functions uselessCriticalSolved - 08/12/2024
Not sending value renders withOraclePricesForAtomicAction modifier uselessCriticalSolved - 08/14/2024
Pyth oracle price is not validated properlyMediumSolved - 08/12/2024

7. Findings & Tech Details

7.1 Mismatch between stored and reported Pyth prices

//

Critical

Description

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.

Proof of Concept

The bug is pretty visual:

https://github.com/relative-finance/rfx-contracts/blob/feb04bf331c97a379556ed2eba30972415980034/contracts/oracle/PythDataStreamProvider.sol#L74

        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:

https://github.com/relative-finance/rfx-contracts/blob/feb04bf331c97a379556ed2eba30972415980034/contracts/oracle/PythDataStreamProvider.sol#L87C1-L95C6

    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.

BVSS
Recommendation

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.

Remediation Plan

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:

https://github.com/relative-finance/rfx-contracts/blob/53b871efdaa63437c8397aa7bae4f3cdb6364ae5/contracts/oracle/PythDataStreamProvider.sol#L69C1-L87C117

            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);
Remediation Hash

7.2 Missing payable keyword renders some functions useless

//

Critical

Description

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.

Proof of Concept

The affected functions are:

https://github.com/relative-finance/rfx-contracts/blob/0466b4a880d7d5b87ff75b639358fbe53db3b313/contracts/oracle/Oracle.sol#L179C1-L184C6

    function validatePrices(
        OracleUtils.SetPricesParams memory params,
        bool forAtomicAction
    ) external onlyController returns (OracleUtils.ValidatedPrice[] memory) {
        return _validatePrices(params, forAtomicAction);
    }

https://github.com/relative-finance/rfx-contracts/blob/0466b4a880d7d5b87ff75b639358fbe53db3b313/contracts/oracle/Oracle.sol#L119C1-L125C6

    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:

https://github.com/relative-finance/rfx-contracts/blob/0466b4a880d7d5b87ff75b639358fbe53db3b313/contracts/oracle/Oracle.sol#L294

            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.

BVSS
Recommendation

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.

Remediation Plan

SOLVED: The Relative Finance team solved this issue by adding the payable keyword in the affected functions:

https://github.com/relative-finance/rfx-contracts/blob/53b871efdaa63437c8397aa7bae4f3cdb6364ae5/contracts/oracle/Oracle.sol#L179C1-L184C6

    function validatePrices(
        OracleUtils.SetPricesParams memory params,
        bool forAtomicAction
    ) external payable onlyController returns (OracleUtils.ValidatedPrice[] memory) {
        return _validatePrices(params, forAtomicAction);
    }

https://github.com/relative-finance/rfx-contracts/blob/53b871efdaa63437c8397aa7bae4f3cdb6364ae5/contracts/oracle/Oracle.sol#L119C1-L125C6

    function setPricesForAtomicAction(
        OracleUtils.SetPricesParams memory params
    ) external payable onlyController {
        OracleUtils.ValidatedPrice[] memory prices = _validatePrices(params, true);

        _setPrices(prices);
    }
Remediation Hash

7.3 Not sending value renders withOraclePricesForAtomicAction modifier useless

//

Critical

Description

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.

Proof of Concept

Pretty visual:

https://github.com/relative-finance/rfx-contracts/blob/53b871efdaa63437c8397aa7bae4f3cdb6364ae5/contracts/oracle/OracleModule.sol#L35C1-L41C6

    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.

BVSS
Recommendation

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.

Remediation Plan

SOLVED: The Relative Finance team solved this issue by sending the required fee alongside the call to the oracle:

https://github.com/relative-finance/rfx-contracts/pull/47/files#diff-ec9855fdb537cbb2d7605899ff9818f4988dbd02b59ccd34f148e721d5c0bc3c

    modifier withOraclePricesForAtomicAction(
        OracleUtils.SetPricesParams memory params
    ) {
        oracle.setPricesForAtomicAction{value: msg.value}(params);
        _;
        oracle.clearAllPrices();
    }

7.4 Pyth oracle price is not validated properly

//

Medium

Description

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.

BVSS
Recommendation

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

Remediation Plan

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.

Remediation Hash
References

8. Automated Testing

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.

© Halborn 2025. All rights reserved.