Halborn Logo

StageZero - OrangeLayer


Prepared by:

Halborn Logo

HALBORN

Last Updated 06/26/2024

Date of Engagement by: May 2nd, 2024 - May 17th, 2024

Summary

94% of all REPORTED Findings have been addressed

All findings

16

Critical

1

High

0

Medium

1

Low

1

Informational

13


1. Introduction

The OrangeLayer team engaged Halborn to conduct a security assessment on their smart contracts beginning on 05/02/2024 and ending on 05/17/2024. The security assessment was scoped to the smart contracts provided in the GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

Halborn was provided 2 weeks for the engagement and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.

In summary, Halborn identified some minor security findings and recommendations that were mostly addressed by the OrangeLayer 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.

    • Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX)

    • Static Analysis of security for scoped contract, and imported functions (slither).

    • Testnet deployment (Foundry).

3.1 Out-of-scope

    • External libraries and financial-related attacks.

    • New features/implementations after/with the remediation commit IDs.The review of the new cancellation mechanism and the deployment scripts was later conducted, uncovering two informational risk findings.


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:
EXPLOITABILIY 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: orangelayer-contracts
(b) Assessed Commit ID: e9c7f51
(c) Items in scope:
  • src/AccessManager.sol
  • src/CoStake.sol
  • src/PauseManager.sol
↓ Expand ↓
Out-of-Scope: src/Timelock.sol
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

1

High

0

Medium

1

Low

1

Informational

13

Security analysisRisk levelRemediation Date
Anyone Can Upgrade ImplementationCriticalSolved - 06/21/2024
Pausing Claims Does Not WorkMediumSolved - 06/21/2024
Use of Deprecated and Non-Official Third-Party LibrariesLowSolved - 06/21/2024
Missing Call to `__ReentrancyGuard_init()` in the initialize() FunctionInformationalSolved - 06/21/2024
Incomplete NatSpec Documentation and Test CoverageInformationalFuture Release
ToDo Comments and Commented Out CodeInformationalPartially Solved - 06/21/2024
Unnecessary Input ValidationInformationalPartially Solved - 06/21/2024
ReentrancyGuard Contract Not Using NonReentrant ModifierInformationalSolved - 06/21/2024
Upgradeable Contract Missing a Storage GapInformationalSolved - 06/21/2024
Cache Array Length Outside of LoopInformationalSolved - 06/21/2024
Unused Modifier and FunctionInformationalSolved - 06/21/2024
Unused Global State VariableInformationalSolved - 06/21/2024
Unnecessary Named Return DeclarationsInformationalAcknowledged
Consider Using Named MappingsInformationalSolved - 06/21/2024
Typo in CommentsInformationalAcknowledged
Unlocked PragmaInformationalAcknowledged

7. Findings & Tech Details

7.1 Anyone Can Upgrade Implementation

// Critical

Description

The StakeManager and ethwBTC contracts contain a critical security vulnerability where any address can upgrade the contract implementation using the upgradeToAndCall() function. While the upgradeTo() function is protected by the onlyUpgrader modifier, the upgradeToAndCall() function lacks this restriction, allowing unauthorized users to change the implementation of the contract.


Proof of Concept

The following Foundry test was used in order to prove the aforementioned issue:

function testEthereumBTCupgradeToAttack() public {
    address attacker = address(1);

    vm.startPrank(attacker);
    // Attacker tries to upgrade the implementation via `upgradeTo()`
    // Should fail as `upgradeTo()` has the `onlyUpgrader` modifier
    vm.expectRevert(AuthenticationFailed.selector);
    ethwBTC.upgradeTo(address(123));

    // Attacker successfully upgrades the via `upgradeToAndCall()`
    // It works as `upgradeToAndCall()` does not have the `onlyUpgrader` modifier
    Upgrades.upgradeProxy(
        address(ethwBTC),
        "EthereumBTCV2.sol:EthwBTCV2",
        abi.encodeCall(EthwBTCV2.initialize, (address(accessManager), address(pauseManager), address(this)))
    );

    vm.stopPrank();
}

To run it, just execute the following forge command:

forge test --mt testEthereumBTCupgradeToAttack --force -vvvv

Observe that the test passes and an arbitrary attacker has changed the implementation of the ethwBTC contract.

[PASS] testEthereumBTCupgradeToAttack() (gas: 3143893)
...
    ├─ [57774] ERC1967Proxy::upgradeToAndCall(EthwBTCV2: [0x522B3294E6d06aA25Ad0f1B8891242E335D3B459], 0xc0c53b8b0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496)
    │   ├─ [52851] EthwBTC::upgradeToAndCall(EthwBTCV2: [0x522B3294E6d06aA25Ad0f1B8891242E335D3B459], 0xc0c53b8b0000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496) [delegatecall]
    │   │   ├─ [343] EthwBTCV2::proxiableUUID() [staticcall]
    │   │   │   └─ ← [Return] 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
    │   │   ├─ emit Upgraded(implementation: EthwBTCV2: [0x522B3294E6d06aA25Ad0f1B8891242E335D3B459])
    │   │   ├─ [45430] EthwBTCV2::initialize(AccessManager: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PauseManager: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a], EthereumBTCTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [delegatecall]
    │   │   │   ├─ emit Initialized(version: 2)
    │   │   │   └─ ← [Stop] 
    │   │   └─ ← [Stop] 
    │   └─ ← [Return] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    └─ ← [Stop] 
BVSS
Recommendation

Consider adding the onlyUpgrader modifier to the _authorizeUpgrade() internal function instead of only to the upgradeTo() external function to ensure that only authorized addresses can execute upgrade functionalities.

For more information, see: https://docs.openzeppelin.com/contracts/5.x/api/proxy#UUPSUpgradeable-_authorizeUpgrade-address-

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by adding the onlyUpgrader modifier to the _authorizeUpgrade function.

Remediation Hash

7.2 Pausing Claims Does Not Work

// Medium

Description

During the security review, it was discovered that, even if a user with the pauser role successfully called pauseClaim() to pause the claiming functionalities, it is still possible to call the claim() and claimBatch() functions. This indicates that the pausing mechanism for claim functions is ineffective, potentially allowing unauthorized claims during the paused state.

This is because none of the two claiming functionalities, claim() and claimBatch(), had the whenNotPausedToken(PAUSE_TOKEN_CLAIM, _token) modifier:

function claim(address _user, uint256 _orderId) external nonReentrant returns (uint256) {

function claimBatch(address _user, uint256[] calldata _orderIds) external nonReentrant returns (uint256) {
Proof of Concept

The following Foundry test was used in order to prove the aforementioned issue:

function testStakeManagerClaimPaused() public {
    vm.startPrank(admin);
    accessManager.grantGovernanceRole(governance);
    accessManager.grantStakeManagerRole(address(stakeManager));
    vm.stopPrank();

    vm.startPrank(governance);
    stakeManager.addTokenToWhitelist(address(ethwBTC));
    stakeManager.setDepositCap(address(wBTCToken), 150 ether);
    vm.stopPrank();

    // Getting some tokens to test
    wBTCToken.mint(address(this), 1_000 ether);
    wBTCToken.approve(address(stakeManager), 500 ether);

    // Deposit & withdraw
    stakeManager.deposit(address(wBTCToken), 100 ether);
    stakeManager.withdraw(address(wBTCToken), 100 ether);

    // Grant permissions and pause token claim
    vm.startPrank(admin);
    assertTrue(accessManager.grantPauserRole(admin));
    pauseManager.pauseClaim((address(wBTCToken)));
    assertTrue(pauseManager.isPausedToken(PAUSE_TOKEN_CLAIM, address(wBTCToken)));
    vm.stopPrank();

    skip(block.timestamp + 5 minutes + 1);
    vm.expectRevert(ActionPaused.selector);
    stakeManager.claim(address(this), 1);
}

To run it, just execute the following forge command:

forge test --mt testStakeManagerClaimPaused --force -vvvv

Observe that the test fails because it does not revert as expected.

[FAIL. Reason: call did not revert as expected] testStakeManagerClaimPaused() (gas: 695149)
...
    ├─ [0] VM::expectRevert(ActionPaused())
    │   └─ ← [Return] 
    ├─ [36667] ERC1967Proxy::claim(StakeManagerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1)
    │   ├─ [36271] StakeManager::claim(StakeManagerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 1) [delegatecall]
    │   │   ├─ [28462] ERC1967Proxy::processClaim(1)
    │   │   │   ├─ [28069] EthwBTC::processClaim(1) [delegatecall]
    │   │   │   │   ├─ [734] AccessManager::isStakeManager(ERC1967Proxy: [0xc7183455a4C133Ae270771860664b6B7ec320bB1]) [staticcall]
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   ├─ [3310] MockToken::transfer(StakeManagerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 100000000000000000000 [1e20])
    │   │   │   │   │   ├─ emit Transfer(from: ERC1967Proxy: [0xA4AD4f68d0b91CFD19687c881e50f3A00242828c], to: StakeManagerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 100000000000000000000 [1e20])
    │   │   │   │   │   └─ ← [Return] true
    │   │   │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   │   ├─ emit LogClaim(_token: MockToken: [0xa0Cb889707d426A7A386870A03bc70d1b0697598], _user: StakeManagerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], _orderId: 1, amountToken: 100000000000000000000 [1e20])
    │   │   └─ ← [Return] 100000000000000000000 [1e20]
    │   └─ ← [Return] 100000000000000000000 [1e20]
    └─ ← [Revert] call did not revert as expected

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.21s (306.54µs CPU time)q
BVSS
Recommendation

Ensure that the claim() and claimBatch() functions check if the claim is paused before executing. This can be done by adding a modifier or an inline check to verify the paused state. A proposed solution would be:


function claim(address _user, uint256 _orderId) external nonReentrant whenNotPausedToken(PAUSE_TOKEN_CLAIM, _token) returns (uint256) {

function claimBatch(address _user, uint256[] calldata _orderIds) external nonReentrant whenNotPausedToken(PAUSE_TOKEN_CLAIM, _token) returns (uint256) {

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by adding the whenNotPausedToken modifier to the claim() and claimBatch() functions.

Remediation Hash

7.3 Use of Deprecated and Non-Official Third-Party Libraries

// Low

Description

The codebase utilizes several outdated or non-official third-party libraries, which could pose security and maintenance risks. Specifically:

  1. The library LowGasSafeMath is being used, although it is redundant with Solidity 0.8.0 and above, which natively handles overflows and underflows.

  2. The library TransferHelper is in use, but it lacks some advanced functionalities provided by OpenZeppelin's SafeERC20, such as safeIncreaseAllowance() and safeDecreaseAllowance().

  3. For testing and deployment, an unofficial third-party dependency foundry-upgrades (see: foundry-upgrades) is used. It is recommended to switch to the official OpenZeppelin counterpart (see: openzeppelin-foundry-upgrades) to ensure better support and security.

BVSS
Recommendation
  1. Remove Redundant Libraries:

    Replace the LowGasSafeMath library with native Solidity arithmetic operations to leverage built-in overflow and underflow checks provided by Solidity 0.8.0 and above. This change will reduce unnecessary dependencies and simplify the codebase.


  2. Adopt Industry-Standard Libraries:

    Replace the TransferHelper library with OpenZeppelin's SafeERC20 library. The SafeERC20 library offers additional functionalities such as safeIncreaseAllowance() and safeDecreaseAllowance(), providing more comprehensive and secure token handling.


  3. Switch to Official Upgrades Library:

    Replace the unofficial foundry-upgrades dependency with the official openzeppelin-foundry-upgrades library from OpenZeppelin. The official library is likely to be more secure, well-maintained, and supported by the community. Remove the foundry-upgrades dependency if it is no longer needed.


By implementing these recommendations, the codebase will be more secure, maintainable, and aligned with industry standards, reducing potential risks associated with using outdated or unofficial third-party libraries.


Remediation Plan

SOLVED: The OrangeLayer team removed the use of the LowGasSafeMath and TransferHelper libraries (although they are still present in the project) and switched the use of the unofficial foundry-upgrades library to the one from OpenZeppelin (openzeppelin-foundry-upgrades library). For the sake of simplicity, it would be recommended to delete the LowGasSafeMath and TransferHelper libraries altogether.

Remediation Hash

7.4 Missing Call to `__ReentrancyGuard_init()` in the initialize() Function

// Informational

Description

Most contracts use the delegateCall proxy pattern, and hence their implementations require the use of initialize() functions instead of constructors. This requires derived contracts to call the corresponding init functions of their inherited base contracts. This is done in most places except in the EthwBTC contract, which is inheriting from ReentrancyGuardUpgradeable, but its initialize() function is missing the call to __ReentrancyGuard_init(). Observe the initialize() function in use:

/**
 * @dev Initializes the contract with required parameters.
 * @param _token Address of the underlying token.
 * @param _accessManager Address of the AccessManager contract.
 * @param _pauseManager Address of the PauseManager contract.
 */
function initialize(address _token, address _accessManager, address _pauseManager) external initializer onlyProxy {
    if (_token == address(0) || _accessManager == address(0) || _pauseManager == address(0))
        revert InputAddressZero();

    __UUPSUpgradeable_init();

    underlyingToken = _token;
    accessManager = IAccessManager(_accessManager);
    pauseManager = IPauseManager(_pauseManager);
}
BVSS
Recommendation

Add the missing __ReentrancyGuard_init() call to the initialize() function of the EthwBTC contract as suggested below:

/**
 * @dev Initializes the contract with required parameters.
 * @param _token Address of the underlying token.
 * @param _accessManager Address of the AccessManager contract.
 * @param _pauseManager Address of the PauseManager contract.
 */
function initialize(address _token, address _accessManager, address _pauseManager) external initializer onlyProxy {
    if (_token == address(0) || _accessManager == address(0) || _pauseManager == address(0))
        revert InputAddressZero();

    __UUPSUpgradeable_init();
    __ReentrancyGuard_init();

    underlyingToken = _token;
    accessManager = IAccessManager(_accessManager);
    pauseManager = IPauseManager(_pauseManager);
}

Remediation Plan

SOLVED: The OrangeLayer team added the missing __ReentrancyGuard_init() call to the initialize() function of the EthwBTC contract as suggested.

Remediation Hash

7.5 Incomplete NatSpec Documentation and Test Coverage

// Informational

Description

At least all public and external functions that are not view or pure should have NatSpec comments. During the security review, it was observed that the multiple publicly-accessible functions did not have any NatSpec comments.

Moreover, the test suite's coverage is insufficient, representing a less-than-ideal practice and failing to ensure thorough validation of function behaviors and overall contract functionality.

Finally, the project could not be compiled directly from the GitHub repository due to compilation errors. These errors had to be resolved before the security review could begin, indicating potential issues with the project's setup. See the following screenshots for more details on the compilation errors:

Compilation errors.pngCompilation errors 2.png
BVSS
Recommendation

Consider adding NatSpec documentation to all functions, specially those that are publicly-accessible.

Additionally, expanding the test suite's coverage is essential to effectively validate function functionalities and ensure thorough contract testing. Enhancing the test suite represents a best practice, instilling confidence in contract behavior, mitigating the risk of undetected bugs, and streamlining the development process. By addressing these areas, the project can foster transparency, reliability, and robustness, thereby enhancing its overall quality and user experience.

Finally, resolve any compilation errors present in the project to ensure that it can be compiled successfully from the GitHub repository.


Remediation Plan

PENDING: The OrangeLayer team indicated that they are currently working on test cases and improving their NatSpec comments.

7.6 ToDo Comments and Commented Out Code

// Informational

Description

The codebase contains several instances of ToDo comments and commented-out code. While these do not directly cause security vulnerabilities or impact the audit outcome as long as the code is functional, their presence can aid attackers in identifying potential attack surfaces. Additionally, the existence of development comments suggests that the audited code may differ from the final released version, increasing the risk of new vulnerabilities being introduced post-audit.

Example of a ToDo comment suggesting a further update of the code:

/**
 * @dev Upgrades the contract to a new implementation.
 * @param _newImplementation The address of the new implementation contract.
 #TODO Pausability check before upgrade
    */
function upgradeTo(address _newImplementation) external onlyProxy onlyUpgrader {
    _authorizeUpgrade(_newImplementation);
    upgradeToAndCall(_newImplementation, "");
}
BVSS
Recommendation

Ensure that all ToDo comments and commented-out lines are removed from the codebase before deployment. These elements can provide insights into unfinished or potentially insecure parts of the code to an attacker.


Remediation Plan

PARTIALLY SOLVED: During the remediation review, it was found that new lines of code were commented out and there are still some ToDo comments.

Remediation Hash

7.7 Unnecessary Input Validation

// Informational

Description

The setWithdrawalDelay() function of the StakeManager contract contains redundant input validation checks that add unnecessary complexity and can increase gas costs. Solidity inherently ensures that the _delay parameter is a valid member of the WithdrawalDelay enum, making these additional checks redundant.

/// @notice Withdrawal delay options.
enum WithdrawalDelay {
  NoDelay,
  BasicDelay,
  DelegationDelay
}

...

function setWithdrawalDelay(WithdrawalDelay _delay) external onlyGovernance whenNotPaused(PAUSE_LISTING_UPDATE) returns (bool) {
  // Input validation
  if (
    _delay != WithdrawalDelay.NoDelay &&
    _delay != WithdrawalDelay.BasicDelay &&
    _delay != WithdrawalDelay.DelegationDelay
  ) revert InvalidWithdrawalDelay();
...

BVSS
Recommendation

It is recommended to eliminate the unnecessary validation checks within the setWithdrawalDelay() function. Solidity guarantees that the _delay parameter is a valid member of the WithdrawalDelay enum, so these checks do not add any additional safety.

Remediation Plan

PARTIALLY SOLVED: While the redundant input validation was indeed removed, during the remediation review it was found that the new validation is way less restrictive now. The _delay parameter was changed from being of type WithdrawalDelay enum to uint. This change means that both validations have been removed (the implicit for using the enum and the explicit that was highlighted as redundant). Now, the affected function has a way more lax validation:

  1. The _delay parameter can contain an arbitrary number.

  2. The condition

if (_delay <= 0) revert InvalidWithdrawalDelay();

could be changed to:

if (_delay == 0) revert InvalidWithdrawalDelay();

as _delay cannot be less than 0 as it is a uint number.

It is recommended to evaluate whether the new permissive withdrawalDelayBlocks allowed values make sense.

Remediation Hash

7.8 ReentrancyGuard Contract Not Using NonReentrant Modifier

// Informational

Description

The EthwBTC contract inherits from ReentrancyGuardUpgradeable to protect against reentrancy attacks. However, it does not use the nonReentrant modifier in any of its functions. This oversight can leave the contract vulnerable to reentrancy attacks, where an attacker repeatedly calls a function before its initial execution is complete, potentially causing unexpected behavior and financial loss.

BVSS
Recommendation

Review all external and public functions in the EthwBTC contract and apply the nonReentrant modifier to functions that involve state changes, external calls, or transfers of Ether or tokens. This ensures protection against reentrancy attacks. If the nonReentrant modifier is not needed, consider removing the ReentrancyGuardUpgradeable inheritance.

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by adding the nonReentrant modifier to protect functions interacting with other contracts.

Remediation Hash

7.9 Upgradeable Contract Missing a Storage Gap

// Informational

Description

The abstract contract CoStake is meant to be inherited by other contracts. However, it does not contain a __gap variable, even though it is upgradeable. This means that in the event of adding new state variables when upgrading the contract, it can lead to storage slot collisions.

Notice that, by inspecting the storage slots of the CoStake contract, the slot 3 is the last one (used by the global state variable withdarwOrderInfo):

forge inspect src/CoStake.sol:CoStake storage --pretty
| Name              | Type                                                  | Slot | Offset | Bytes | Contract                |
|-------------------|-------------------------------------------------------|------|--------|-------|-------------------------|
| underlyingToken   | address                                               | 0    | 0      | 20    | src/CoStake.sol:CoStake |
| totalShares       | uint256                                               | 1    | 0      | 32    | src/CoStake.sol:CoStake |
| userInfos         | mapping(address => struct ICoStake.UserInfo)          | 2    | 0      | 32    | src/CoStake.sol:CoStake |
| withdarwOrderInfo | mapping(uint256 => struct ICoStake.WithdarwOrderInfo) | 3    | 0      | 32    | src/CoStake.sol:CoStake |

Notice that EthwBTC also has slot 3 used by the global state variable withdarwOrderInfo, and then its own global state variables:

forge inspect src/networks/EthereumBTC.sol:EthwBTC storage --pretty
| Name              | Type                                                  | Slot | Offset | Bytes | Contract                         |
|-------------------|-------------------------------------------------------|------|--------|-------|----------------------------------|
| underlyingToken   | address                                               | 0    | 0      | 20    | networks/EthereumBTC.sol:EthwBTC |
| totalShares       | uint256                                               | 1    | 0      | 32    | networks/EthereumBTC.sol:EthwBTC |
| userInfos         | mapping(address => struct ICoStake.UserInfo)          | 2    | 0      | 32    | networks/EthereumBTC.sol:EthwBTC |
| withdarwOrderInfo | mapping(uint256 => struct ICoStake.WithdarwOrderInfo) | 3    | 0      | 32    | networks/EthereumBTC.sol:EthwBTC |
| accessManager     | contract IAccessManager                               | 4    | 0      | 20    | networks/EthereumBTC.sol:EthwBTC |
| pauseManager      | contract IPauseManager                                | 5    | 0      | 20    | networks/EthereumBTC.sol:EthwBTC |

In the event of adding a new global state variable to CoStake, it could enter into conflict with EthwBTC.

For more information, see: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps

BVSS
Recommendation

It is recommended to declare a fixed-size array in the base contract with an initial number of slots. This can be an array of uint256 so that each element reserves a 32 byte slot. Use the name __gap or a name starting with __gap_ for the array so that OpenZeppelin Upgrades will recognize the gap:

abstract contract CoStake is ICoStake {
  ...
  uint256[46] __gap;
}

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by adding a __gap global state variable at the end of the CoStake abstract contract.

Remediation Hash

7.10 Cache Array Length Outside of Loop

// Informational

Description

When the length of an array is not cached outside of a loop, the Solidity compiler reads the length of the array during each iteration. For storage arrays, this results in an extra sload operation (100 additional gas for each iteration except the first). For memory arrays, this results in an extra mload operation (3 additional gas for each iteration except the first).

Detecting loops that use the length member of a storage array in their loop condition without modifying it can reveal opportunities for optimization.

// Find the index of the token in the array
uint256 indexToRemove;
for (uint256 i = 0; i < tokenList.length; i++) {
    if (tokenList[i] == _token) {
        indexToRemove = i;
        break;
    }
}

BVSS
Recommendation

Cache the length of the array in a local variable outside the loop to optimize gas usage. This reduces the number of read operations required during each iteration of the loop.

// Find the index of the token in the array
uint256 indexToRemove;
uint256 tokenListLength = tokenList.length;
for (uint256 i = 0; i < tokenListLength; i++) {
    if (tokenList[i] == _token) {
        indexToRemove = i;
        break;
    }
}

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by updating how the removeTokenFromWhitelist() works. During the remediation review it was found that the affected for loop is now commented out. While this finding has been fixed, consider removing all commented out code as indicated in the finding ToDo Comments and Commented Out Code.

Remediation Hash

7.11 Unused Modifier and Function

// Informational

Description

The whenNotPaused modifier and the related _whenNotPaused() function of the EthwBTC contract are not utilized by any of the functions. Additionally, the _whenNotPaused() function is empty, as its body is commented out. This indicates that the contract is not leveraging the intended pause functionality, potentially leaving it without the ability to restrict actions during paused states.

/**
 * @dev Modifier to check if the contract is not paused by a specific pauser. 
 * @param _pauser The address of the pauser.
 */
modifier whenNotPaused(bytes32 _pauser) {
  _whenNotPaused(_pauser);
  _;
}

...

/**
 * @dev Checks if the contract is not paused by a specific pauser.
 * @param _pauser The address of the pauser.
 */
function _whenNotPaused(bytes32 _pauser) internal view {
    // if (!pauseManager.isPaused(_pauser)) revert ActionPaused();
}

BVSS
Recommendation

If the pause functionality is not required, consider removing the whenNotPaused modifier and the _whenNotPaused() function from the contract to reduce unnecessary complexity and maintain a clean codebase. Otherwise, uncomment and properly implement the _whenNotPaused() function to check the paused state. This will enable the whenNotPaused modifier to effectively restrict function calls.

Finally, ensure that the pause functionality is well-documented, explaining when and how the contract can be paused and unpaused, and the implications for the contract's operations. This helps developers and auditors understand the intended behavior and ensures proper usage.

By following these recommendations, the contract will either effectively utilize the pause functionality to enhance security and control or maintain a clean codebase by removing unnecessary components.


Remediation Plan

SOLVED: The OrangeLayer team solved the issue by making use of the whenNotPaused modifier.

Remediation Hash

7.12 Unused Global State Variable

// Informational

Description

In the StakeManager contract, there is a global state variable declared named claimDelay. However, upon review, it appears that this variable is never referenced or utilized within the contract's functions or logic.

// Claim delay
uint256 internal claimDelay;

BVSS
Recommendation

Unused variables should be cleaned up from the code if they have no purpose. Clearing these variables can reduce gas usage during the deployment of contracts, and also increases the readability of the code.

More specifically, consider removing the unused claimDelay variable to declutter the contract codebase and improve readability. Alternatively, if claimDelay serves a purpose that is not immediately evident, ensure that it is properly integrated into the contract's functionality to avoid unnecessary complexity and maintain code efficiency.

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by commenting the unused state variable out. While this finding has been fixed, consider removing all commented out code as indicated in the finding ToDo Comments and Commented Out Code.

Remediation Hash

7.13 Unnecessary Named Return Declarations

// Informational

Description

In CoStake.sol, there are instances where functions declare named return variables but do not use them, resulting in redundant code. Specifically:

The function _getUserDetails declares UserInfo memory userInfo but returns userInfos[_user] instead of using the named return variable.

function _getUserDetails(address _user) internal view returns (UserInfo memory userInfo){
  return userInfos[_user];
}

The function _getWithdrawOrderInfo declares WithdrawOrderInfo memory woi but returns withdrawOrderInfo[_orderId] instead of using the named return variable.

function _getWithdrawOrderInfo(uint256 _orderId) internal view returns (WithdarwOrderInfo memory woi){
  return withdarwOrderInfo[_orderId];
}

These declarations are unnecessary and can be simplified.

Finally, in the related functions of the StakeManager contract, named returns are not used, even though they could to simplify things further:

function getUserDetails(address _token) external view returns (ICoStake.UserInfo memory){
    // Retrieve user details from the staking contract
    ICoStake.UserInfo memory userinfo;
    userinfo = ICoStake(tokensMeta[_token].stakerCore).getUserDetails(msg.sender);
    return userinfo;
}

and:

function getOrderIdDetails(address _token, uint256 _orderId) public view returns (ICoStake.WithdarwOrderInfo memory) {
    // Retrieve withdrawal order details from the staking contract
    ICoStake.WithdarwOrderInfo memory withdarwOrderInfo;
    withdarwOrderInfo = ICoStake(tokensMeta[_token].stakerCore).getOrderIdDetails(_orderId);
    return withdarwOrderInfo;
}
BVSS
Recommendation

Consider removing the unused named return variables and just specifying the return type in the function signature.

function _getUserDetails(address _user) internal view returns (UserInfo memory) {
  return userInfos[_user];
}
function _getWithdrawOrderInfo(uint256 _orderId) internal view returns (WithdarwOrderInfo memory) {
  return withdarwOrderInfo[_orderId];
}

Finally, notice the related function on StakeManager could be simplified too by doing:

function getUserDetails(address _token) external view returns (ICoStake.UserInfo memory userinfo) {
    // Retrieve user details from the staking contract
    userinfo = ICoStake(tokensMeta[_token].stakerCore).getUserDetails(msg.sender);
}

and

function getOrderIdDetails(address _token, uint256 _orderId) public view returns (ICoStake.WithdarwOrderInfo memory withdarwOrderInfo) {
    // Retrieve withdrawal order details from the staking contract
    withdarwOrderInfo = ICoStake(tokensMeta[_token].stakerCore).getOrderIdDetails(_orderId);
}

Remediation Plan

ACKNOWLEDGED: The OrangeLayer team acknowledged this finding by leaving the mentioned functions intact.

7.14 Consider Using Named Mappings

// Informational

Description

The project is using Solidity version greater than 0.8.18, which supports named mappings. Using named mappings can improve the readability and maintainability of the code by making the purpose of each mapping clearer. This practice helps developers and auditors understand the mappings' intent more easily.

BVSS
Recommendation

Consider refactoring the mappings to use named arguments, which will enhance code readability and make the purpose of each mapping more explicit.

For example, on StakeManager, instead of declaring:

mapping(address => uint256[]) public userOrdersList;

It could be declared as:

mapping(address user => uint256[] orderList) public userOrdersList;

Remediation Plan

SOLVED: The OrangeLayer team solved the issue by using named mappings as suggested.

Remediation Hash

7.15 Typo in Comments

// Informational

Description

The following comment was found in the CoStake contract:

// TODO snaity check to make sure token are transferred

The word snaity above should be spelled as sanity instead.

Score
Recommendation

To maintain clarity and trustworthiness, it is essential to rectify any typographical errors present within the contracts. Correcting such errors minimizes the likelihood of confusion and reinforces confidence in the accuracy and integrity of the documentation.

Remediation Plan

ACKNOWLEDGED: This finding has been marked as acknowledged because there are still some typographical errors in the code.

7.16 Unlocked Pragma

// Informational

Description

The EthereumBTC.sol file currently uses floating pragma version ^0.8.20, which means that the code can be compiled by any compiler version that is greater than or equal to 0.8.20, and less than 0.9.0.

It is recommended that contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the contract system negatively.

Score
Recommendation

Consider locking the pragma version to the same version used during development and testing.


Remediation Plan

ACKNOWLEDGED: The OrangeLayer team acknowledged this finding by not fixing it leaving pragma version as >=0.8.20 <0.9.0. This would allow the contracts to be compiled by any compiler version that is greater than or equal to 0.8.20, and less than 0.9.0.

8. Automated Testing

Static Analysis Report

Description

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.

All issues identified by Slither were proved to be false positives or have been added to the issue list in this report.

Output

Slither OrangeLayer StageZero 1.png
Slither OrangeLayer StageZero 2.png

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.