Halborn Logo

Exchange - LPBase


Prepared by:

Halborn Logo

HALBORN

Last Updated 08/08/2024

Date of Engagement by: April 25th, 2024 - June 6th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

10

Critical

0

High

0

Medium

1

Low

1

Informational

8


1. Introduction

The LPBase team engaged Halborn to conduct a security assessment on their smart contracts beginning on 04/25/2024 and ending on 05605/2024. The security assessment was scoped to the smart contracts. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

Halborn was provided 5 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 experts 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 security that were mostly addressed by the LPBase 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).


3.1 Out-of-scope

    • External libraries and financial-related attacks.

    • New features/implementations after/with the remediation commit IDs.

    • Changes that occur outside the scope of PRs.

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
Out-of-Scope:
Files and Repository
(a) Repository: contracts
(b) Assessed Commit ID: a2a3e6d
(c) Items in scope:
    Out-of-Scope:
    Remediation Commit ID:
    Out-of-Scope: New features/implementations after the remediation commit IDs.

    6. Assessment Summary & Findings Overview

    Critical

    0

    High

    0

    Medium

    1

    Low

    1

    Informational

    8

    Security analysisRisk levelRemediation Date
    Sandwich Attack in convertTokens FunctionMediumRisk Accepted
    Lack of Upper Bound on Execution FeeLowSolved - 08/01/2024
    Accounting Errors with Deflationary TokensInformationalAcknowledged
    Inability to Remove Tokens from Pool Leading to Maximum Asset LimitInformationalAcknowledged
    Use Ownable2StepUpgradeable for Secure Ownership TransferInformationalAcknowledged
    Unlocked PragmaInformationalSolved - 06/24/2024
    Incomplete Implementation of getAssetAum FunctionInformationalSolved - 06/25/2024
    Insufficient Gas Limit for ETH Transfer in ETHUnwrapper ContractInformationalSolved - 06/24/2024
    Typo in Comment for Liquidity Execution Fee SettingInformationalSolved - 08/01/2024
    Commented Out Expiration Field in LiquidityOrder StructInformationalAcknowledged

    7. Findings & Tech Details

    7.1 Sandwich Attack in convertTokens Function

    // Medium

    Description

    The convertTokens function calls the swap function of the IPool interface with the minAmount parameter set to 0. By setting minAmount to 0, the function is essentially allowing any amount of slippage during the swap. This can expose the function to a sandwich attack, where an attacker can manipulate the price of the assets being swapped by executing transactions before and after the vulnerable transaction.


        function _convertToken(address _fromToken, address _toToken, address _pool, uint256 _fromAmount) internal returns (uint256 _toAmount) {
            if (_fromAmount == 0) {
                return 0;
            }
            uint256 before = IERC20(_toToken).balanceOf(address(this));
            IERC20(_fromToken).safeTransfer(_pool, _fromAmount);
            IPool(_pool).swap(DEFAULT_INTEGRATOR, _fromToken, _toToken, 0, address(this), address(this));
            _toAmount = IERC20(_toToken).balanceOf(address(this)) - before;
        }
    Proof of Concept

    Here's how a potential attack could occur:

    - The distributeFee function is called, and it initiates a swap from _token to basePool with minAmount set to 0.

    - An attacker observes this transaction in the mempool and quickly executes their own transactions to manipulate the price of _token and basePool.

    - The attacker's transactions are executed before the distributeFee swap, causing the price of _token to increase and the price of basePool to decrease.

    - The distributeFee swap is executed with the manipulated prices, resulting in a lower amount of basePool tokens received than expected.

    - The attacker then executes another set of transactions to revert the price manipulation, profiting from the difference in the swapped amounts.

    BVSS
    Recommendation

    To mitigate the risk of a sandwich attack, it is recommended to set a reasonable minAmount value when calling the swap function. Instead of setting it to 0, consider calculating a minimum acceptable amount based on the current market conditions and the desired slippage tolerance.


    Remediation Plan

    RISK ACCEPTED: The LPBase team accepted the risk of the issue.

    References

    7.2 Lack of Upper Bound on Execution Fee

    // Low

    Description

    The current implementation of the LiquidityRouter contract lacks an upper bound on the execution fee. This could result in excessive fees being charged for executing orders. Without a limit, there's a potential for misuse where very high fees could be set, leading to "fee paid" orders that may not be economically viable for users.

    BVSS
    Recommendation

    To address this issue, it is recommended to introduce an upper bound on the execution fee. This can be done by adding a maximum allowable fee parameter and ensuring that any set fee does not exceed this limit. Additionally, include a function to update this maximum fee, with appropriate access control to prevent unauthorized changes.


    Remediation Plan

    SOLVED: The LPBase team solved the issue.

    Remediation Hash

    7.3 Accounting Errors with Deflationary Tokens

    // Informational

    Description

    The current implementation of the OrderManager contract may lead to accounting errors when dealing with deflationary tokens. When placing a swap order using a deflationary token, the executeSwapOrder() function attempts to swap the same amount of tokens that were initially transferred during the order placement. However, due to the deflationary nature of the token, the actual received amount may be lower than the intended swap amount. This discrepancy can cause the transaction to revert if the OrderManager contract doesn't have a sufficient token balance to fulfill the swap. Additionally, when canceling an order using the cancelOrder() function, returning the funds to the order owner may fail if the OrderManager contract's token balance is insufficient due to the deflationary token's behavior.

    BVSS
    Recommendation

    To address this issue, consider the following approaches:

    Determine whether deflationary tokens will be supported by the OrderManager contract. If support for deflationary tokens is desired, modify the contract to handle them properly. Instead of relying on the amount specified in the transfer() call, use the contract's actual token balance to determine the amount to swap or return. This can be achieved by calculating the difference in the contract's token balance before and after the transfer. If deflationary tokens are not intended to be supported, implement a validation mechanism to prevent their usage. For example, you can check if the received amount matches the expected amount after a transfer and revert the transaction if there is a mismatch.


    Remediation Plan

    ACKNOWLEDGED: The LPBase team acknowledged this finding.

    References

    7.4 Inability to Remove Tokens from Pool Leading to Maximum Asset Limit

    // Informational

    Description

    The addToken() function allows the contract owner to add new tokens to the pool. The token's address is added to the allAssets array, and the token is marked as listed and whitelisted. However, there is no corresponding function to completely remove a token from the pool. The delistToken() function only marks the token as unlisted and sets its target weight to zero, but the token remains in the allAssets array. This means that once the MAX_ASSETS threshold is reached (currently set at 10 total assets), no new tokens can be added, and existing tokens cannot be removed, even if they are delisted.


        function addToken(address _token, bool _isStableCoin, bool _isPool) external onlyOwner {
            if (!isAsset[_token]) {
                isAsset[_token] = true;
                isListed[_token] = true;
                allAssets.push(_token);
                isStableCoin[_token] = _isStableCoin;
                isPool[_token] = _isPool;
                if (allAssets.length > MAX_ASSETS) {
                    revert PoolErrors.TooManyTokenAdded(allAssets.length, MAX_ASSETS);
                }
                emit TokenWhitelisted(_token);
                return;
            }
    
            if (isListed[_token]) {
                revert PoolErrors.DuplicateToken(_token);
            }
    
            // token is added but not listed
            isListed[_token] = true;
            emit TokenWhitelisted(_token);
        }
    
    Score
    Recommendation

    Consider fixing the logic on the addAsset function.


    Remediation Plan

    ACKNOWLEDGED: The LPBase team acknowledged this finding.

    References

    7.5 Use Ownable2StepUpgradeable for Secure Ownership Transfer

    // Informational

    Description

    The current implementation of the contracts inherits from OwnableUpgradeable, which provides a basic ownership mechanism. However, this approach has a potential risk: if the owner accidentally transfers ownership to an incorrect address, the contract's ownership may be permanently lost.


    contract OrderManager is Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable {}
    Score
    Recommendation

    To mitigate this risk, it is recommended to use the Ownable2StepUpgradeable contract from the OpenZeppelin library instead of OwnableUpgradeable. Ownable2StepUpgradeable provides a two-step ownership transfer process, which adds an extra layer of security to prevent accidental ownership transfers.


    Remediation Plan

    ACKNOWLEDGED: The LPBase team acknowledged this finding.

    References

    7.6 Unlocked Pragma

    // Informational

    Description

    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.


    pragma solidity ^0.8.15;
    Score
    Recommendation

    Consider locking the pragma version in the smart contracts. It is not recommended to use a floating pragma in production.

    For example: pragma solidity 0.8.21;


    Remediation Plan

    SOLVED: The LPBase team solved the issue by locking pragma.

    Remediation Hash

    7.7 Incomplete Implementation of getAssetAum Function

    // Informational

    Description

    The provided code snippet contains a function named getAssetAum that is intended to calculate the Assets Under Management (AUM) for a specific token in a pool. However, the implementation of the function is incomplete and currently returns a hardcoded value of 0.

    The function takes three parameters:

    • poolAddress: The address of the pool.

    • token: The address of the token for which the AUM needs to be calculated.

    • _max: A boolean flag indicating whether to use the maximum price or not.

    The function is marked as external and view, suggesting that it is intended to be called from outside the contract and does not modify the contract's state.

    The issue with the current implementation is that it does not perform the actual AUM calculation and instead returns a constant value of 0. The commented-out code block below the return 0 statement indicates that there is an intended logic for calculating the AUM based on various factors such as the pool's stability, token price, pool amount, reserved amount, guaranteed value, and total short size. However, this code is currently commented out and not executed.

    Score
    Recommendation

    To address this issue and complete the implementation of the getAssetAum function, the following steps should be taken:

    1. Uncomment the code block below the return 0 statement to enable the actual AUM calculation logic.


    Remediation Plan

    SOLVED: The LPBase team solved the issue by applying the suggested recommendation.

    Remediation Hash
    References

    7.8 Insufficient Gas Limit for ETH Transfer in ETHUnwrapper Contract

    // Informational

    Description

    The ETHUnwrapper contract is designed to unwrap WETH (Wrapped ETH) tokens and transfer the resulting ETH to a specified address. However, there is an issue with the gas limit set for the ETH transfer operation, which prevents Gnosis Safe contracts from interacting with the unwrap function.

    In the safeTransferETH function, the ETH transfer is performed using a low-level call with a fixed gas limit of 2400:

    (bool success,) = *to.call{gas:2400, value: *amount}("");

    The gas limit of 2400 is insufficient for Gnosis Safe contracts to successfully receive the ETH transfer. Gnosis Safe contracts require a higher gas limit to execute their internal logic and process the incoming ETH transfer.

    Score
    Recommendation

    Replace the fixed gas limit of 2400 with a higher value that is sufficient for Gnosis Safe contracts to process the ETH transfer successfully.


    Remediation Plan

    SOLVED: The LPBase team solved the issue by applying the suggested recommendation.

    Remediation Hash
    References

    7.9 Typo in Comment for Liquidity Execution Fee Setting

    // Informational

    Description

    There is a typo in the comment intended to describe the setting of the liquidity execution fee. The word "liquidity" is misspelled as "liuqidity".

    Location:

    The typo appears in a comment within the onlyOperator modifier.

    Current code:

    modifier onlyOperator() {
    
    if (operator != msg.sender && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)){ // set liuqidity execution fee
    
    revert LiquidityErrors.OnlyOperator();
    
    }
    
    _;
    
    }

    Correct code:

    modifier onlyOperator() {
    
        if (operator != msg.sender && !hasRole(DEFAULT_ADMIN_ROLE, msg.sender)){ // set liquidity execution fee
    
            revert LiquidityErrors.OnlyOperator();
    
        }
    
        _;
    
    }

    Score
    Recommendation

    Correct the spelling of "liquidity" in the comment.


    Remediation Plan

    SOLVED: The LPBase team solved the issue.

    Remediation Hash

    7.10 Commented Out Expiration Field in LiquidityOrder Struct

    // Informational

    Description

    In the LiquidityOrder struct, the expiresAt field is commented out:

    struct LiquidityOrder {
    
        // ... other fields ...
    
        // uint256 expiresAt;
    
        // uint256 executionFee;
    
    }

    The absence of an expiration time for liquidity orders could lead to the following issue :

    1. Without an expiration, orders could remain valid indefinitely, potentially being executed under market conditions very different from when they were created.

    Score
    Recommendation

    1. Re-enable the expiresAt field in the LiquidityOrder struct:

      struct LiquidityOrder {
    
           // ... other fields ...
    
           uint256 expiresAt;
    
           uint256 executionFee;
    
       }


    Remediation Plan

    ACKNOWLEDGED: The LPBase team acknowledged the issue.

    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.