Halborn Logo

WeightedLiquidityPool - Dexodus


Prepared by:

Halborn Logo

HALBORN

Last Updated 11/25/2024

Date of Engagement by: November 18th, 2024 - November 20th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

15

Critical

0

High

1

Medium

1

Low

7

Informational

6


1. Introduction

Dexodus engaged our security analysis team to conduct a comprehensive security audit of their smart contract ecosystem. The primary aim was to meticulously assess the security architecture of the smart contracts to pinpoint vulnerabilities, evaluate existing security protocols, and offer actionable insights to bolster security and operational efficacy of their smart contract framework. Our assessment was strictly confined to the smart contracts provided, ensuring a focused and exhaustive analysis of their security features.

2. Assessment Summary

Our engagement with Dexodus spanned a 3 days period, during which we dedicated one full-time security engineer equipped with extensive experience in blockchain security, advanced penetration testing capabilities, and profound knowledge of various blockchain protocols. The objectives of this assessment were to:

- Verify the correct functionality of smart contract operations.

- Identify potential security vulnerabilities within the smart contracts.

- Provide recommendations to enhance the security and efficiency of the smart contracts.

3. Test Approach and Methodology

Our testing strategy employed a blend of manual and automated techniques to ensure a thorough evaluation. While manual testing was pivotal for uncovering logical and implementation flaws, automated testing offered broad code coverage and rapid identification of common vulnerabilities. The testing process included:

- A detailed examination of the smart contracts' architecture and intended functionality.

- Comprehensive manual code reviews and walkthroughs.

- Functional and connectivity analysis utilizing tools like Solgraph.

- Customized script-based manual testing and testnet deployment using Foundry.

This executive summary encapsulates the pivotal findings and recommendations from our security assessment of Dexodus smart contract ecosystem. By addressing the identified issues and implementing the recommended fixes, Dexodus can significantly boost the security, reliability, and trustworthiness of its smart contract platform.

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: dexodus
(b) Assessed Commit ID: a7e4405
(c) Items in scope:
  • src/DexodusV2/WeightedLiquidityPool.sol
Out-of-Scope:
Remediation Commit ID:
  • 1f89558
  • 2543d43
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

1

Medium

1

Low

7

Informational

6

Security analysisRisk levelRemediation Date
Incorrect Interface Used for Uniswap Interactions on Base NetworkHighSolved - 11/21/2024
Edge Case Leading to Inconsistent LP Share MintingMediumSolved - 11/22/2024
Missing Upper Bound ValidationLowSolved - 11/21/2024
Allowing Zero Value Locks and MintingLowSolved - 11/21/2024
Division by Zero RiskLowSolved - 11/21/2024
Price Discrepancy Risk Between Chainlink Oracle and UniswapLowRisk Accepted - 11/21/2024
Inability to Update priceFeed and Static Decimal HandlingLowSolved - 11/21/2024
Missing Staleness Check in getEthPriceInUSDLowSolved - 11/21/2024
Missing Minimal Liquidity Check in addLiquidityLowSolved - 11/21/2024
Missing Zero Address and ERC165 Interface Checks in InitializerInformationalSolved - 11/21/2024
Inefficient String Comparison in lockLiquidityInformationalSolved - 11/21/2024
Missing Bounds CheckInformationalSolved - 11/21/2024
Lack of Decimal Clarity in Function DocumentationInformationalSolved - 11/21/2024
Lack of Consistent Naming for Internal FunctionsInformationalSolved - 11/21/2024
Inconsistent NamingInformationalSolved - 11/21/2024

7. Findings & Tech Details

7.1 Incorrect Interface Used for Uniswap Interactions on Base Network

// High

Description

The contract uses the ISwapRouter interface from the v3-periphery package for Uniswap interactions. This interface includes a deadline parameter in the ExactInputSingleParams struct, which does not match the deployed Uniswap router on the Base network (address: 0x2626664c2603336E57B271c5C0b26F421741e481). The correct interface for this router, as specified in the Uniswap documentation, is IV3SwapRouter from the swap-router-contracts repository, which excludes the deadline parameter.

This mismatch in interfaces causes all Uniswap interactions, including swaps and rebalancing, to revert. Specifically, the struct used in the Base router differs from the v3-periphery struct in that it does not include a deadline field.

Incorrect Struct from v3-periphery:

struct ExactInputSingleParams {
    address tokenIn;
    address tokenOut;
    uint24 fee;
    address recipient;
    uint256 deadline;
    uint256 amountIn;
    uint256 amountOutMinimum;
    uint160 sqrtPriceLimitX96;
}

Correct Struct from Base Router (IV3SwapRouter):

struct ExactInputSingleParams {
    address tokenIn;
    address tokenOut;
    uint24 fee;
    address recipient;
    uint256 amountIn;
    uint256 amountOutMinimum;
    uint160 sqrtPriceLimitX96;
}
Proof of Concept

Using the provided test case (forge test --fork-url https://mainnet.base.org --match-test test_halborn_rebalance -vvv), it can be demonstrated that rebalancing fails due to the interface mismatch. The issue is resolved when switching to the correct IV3SwapRouter interface and removing the deadline parameter.

function test_halborn_rebalance() public {

    deal(address(_weth), address(liquidityPool), 1 ether / 10); // 310 USDC
    deal(address(_usdc), address(liquidityPool), 1000e6 / 10); // 100 USDC

    vm.startPrank(deployer);

    liquidityPool.setswapSlippage(9_000); // 90%

    // ETH / USDC
    liquidityPool.rebalancePool(50, 50);

    vm.stopPrank();
}
BVSS
Recommendation
  • Replace the ISwapRouter interface from v3-periphery with the correct IV3SwapRouter interface from swap-router-contracts, which matches the deployed router on the Base network.

  • Update all Uniswap interaction calls to remove the deadline parameter from the ExactInputSingleParams struct to align with the Base router's requirements.

  • Ensure compatibility with the Base network router to allow swaps and rebalancing functions to work as intended.

  • Validate the fixes with the provided test case to confirm that swaps and rebalancing operations succeed.

Remediation

SOLVED: The code is now using the IV3SwapRouter interface that matches the one deployed in Base.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.2 Edge Case Leading to Inconsistent LP Share Minting

// Medium

Description

When getEffectivePoolValue approaches zero due to unrealized profits held by traders, the LP share calculations in calcLpOut exhibit inconsistent behavior. Specifically:

  1. Inflated LP Shares: As getEffectivePoolValue approaches zero (but remains slightly positive), the calcLpOut function mints excessively high LP shares.

  2. Reset to Base Shares: When getEffectivePoolValue drops to exactly zero, LP shares return to baseline behavior, ignoring the unrealized profit adjustment.

This discrepancy creates a significant bug where LP providers may inadvertently mint disproportionately high shares in a near-zero pool state. While this scenario may be rare due to protocol mechanisms, it represents a critical vulnerability for the integrity of LP share calculations.

Proof of Concept
    int256 public unrealizedPnL;

    function _setUnrealizedPnL(int256 _unrealizedPnL) public {
        unrealizedPnL = _unrealizedPnL;
    }

    function test_halborn_002() public {
        uint256 eth_price;

        vm.startPrank(deployer);
        liquidityPool.setFuturesCore(address(this));
        vm.stopPrank();

        eth_price = liquidityPool.getEthPriceInUSD(1 ether);
        _setUnrealizedPnL(0);

        // deal(address(_usdc), address(liquidityPool), 1000e6);
        // deal(address(_weth), address(liquidityPool), 1 ether);
        address user = address(0x1001);
        address user2 = address(0x1002);
        address user3 = address(0x1003);

        console.log("USDC balance of pool", IERC20(_usdc).balanceOf(address(liquidityPool)));
        console.log("USDC balance of user", IERC20(_usdc).balanceOf(user));
        console.log("WETH balance of pool", IERC20(_weth).balanceOf(address(liquidityPool)));
        console.log("WETH balance of user", IERC20(_weth).balanceOf(user));
        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
        console.log("Pool balance of user2", IERC20(address(liquidityPool)).balanceOf(user2));
        console.log("getEffectivePoolValue", liquidityPool.getEffectivePoolValue());
        console.log("calcLpOut", liquidityPool.calcLpOut(liquidityPool.getEthPriceInUSD(1 ether)));

        // getEthPriceInUSD
        console.log("getEthPriceInUSD", liquidityPool.getEthPriceInUSD(1 ether));

        console.log("");

        vm.startPrank(user);
        deal(user, 1 ether);
        liquidityPool.lockLiquidity{value: 1 ether}(4, "Crab");
        vm.stopPrank();

        vm.startPrank(user2);
        deal(user2, 1 ether);
        liquidityPool.lockLiquidity{value: 1 ether}(4, "Crab");
        vm.stopPrank();


        console.log("USDC balance of pool", IERC20(_usdc).balanceOf(address(liquidityPool)));
        console.log("USDC balance of user", IERC20(_usdc).balanceOf(user));
        console.log("WETH balance of pool", IERC20(_weth).balanceOf(address(liquidityPool)));
        console.log("WETH balance of user", IERC20(_weth).balanceOf(user));
        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
        console.log("Pool balance of user2", IERC20(address(liquidityPool)).balanceOf(user2));
        console.log("getEffectivePoolValue", liquidityPool.getEffectivePoolValue());
        console.log("calcEthOut user", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user)));
        console.log("calcEthOut user2", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user2)));
        console.log("calcLpOut", liquidityPool.calcLpOut(liquidityPool.getEthPriceInUSD(1 ether)));
        console.log("");


        eth_price = liquidityPool.getEthPriceInUSD(1 ether);
        _setUnrealizedPnL(int256(1 * eth_price));

        console.log("USDC balance of pool", IERC20(_usdc).balanceOf(address(liquidityPool)));
        console.log("USDC balance of user", IERC20(_usdc).balanceOf(user));
        console.log("WETH balance of pool", IERC20(_weth).balanceOf(address(liquidityPool)));
        console.log("WETH balance of user", IERC20(_weth).balanceOf(user));
        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
        console.log("Pool balance of user2", IERC20(address(liquidityPool)).balanceOf(user2));
        console.log("getEffectivePoolValue", liquidityPool.getEffectivePoolValue());
        console.log("calcEthOut user", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user)));
        console.log("calcEthOut user2", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user2)));
        console.log("calcLpOut", liquidityPool.calcLpOut(liquidityPool.getEthPriceInUSD(1 ether)));
        console.log("");

        // vm.startPrank(user3);
        // deal(user3, 1 ether);
        // liquidityPool.lockLiquidity{value: 1 ether}(4, "Crab");
        // vm.stopPrank();

        // console.log("Pool balance of user3", IERC20(address(liquidityPool)).balanceOf(user3));
        // console.log("calcEthOut user3", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user3)));

        console.log("");

        eth_price = liquidityPool.getEthPriceInUSD(1 ether);
        _setUnrealizedPnL(int256(2 * eth_price) - 1);

        console.log("USDC balance of pool", IERC20(_usdc).balanceOf(address(liquidityPool)));
        console.log("USDC balance of user", IERC20(_usdc).balanceOf(user));
        console.log("WETH balance of pool", IERC20(_weth).balanceOf(address(liquidityPool)));
        console.log("WETH balance of user", IERC20(_weth).balanceOf(user));
        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
        console.log("Pool balance of user2", IERC20(address(liquidityPool)).balanceOf(user2));
        console.log("getEffectivePoolValue", liquidityPool.getEffectivePoolValue());
        console.log("calcEthOut user", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user)));
        console.log("calcEthOut user2", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user2)));
        console.log("calcLpOut", liquidityPool.calcLpOut(liquidityPool.getEthPriceInUSD(1 ether)));
        console.log("");

        eth_price = liquidityPool.getEthPriceInUSD(1 ether);
        _setUnrealizedPnL(int256(2 * eth_price));

        console.log("");

        console.log("USDC balance of pool", IERC20(_usdc).balanceOf(address(liquidityPool)));
        console.log("USDC balance of user", IERC20(_usdc).balanceOf(user));
        console.log("WETH balance of pool", IERC20(_weth).balanceOf(address(liquidityPool)));
        console.log("WETH balance of user", IERC20(_weth).balanceOf(user));
        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
        console.log("Pool balance of user2", IERC20(address(liquidityPool)).balanceOf(user2));
        console.log("getEffectivePoolValue", liquidityPool.getEffectivePoolValue());
        console.log("calcEthOut user", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user)));
        console.log("calcEthOut user2", liquidityPool.calcEthOut(IERC20(address(liquidityPool)).balanceOf(user2)));
        console.log("calcLpOut", liquidityPool.calcLpOut(liquidityPool.getEthPriceInUSD(1 ether)));
        console.log("");
    }

Output:

Logs:
  USDC balance of pool 0
  USDC balance of user 0
  WETH balance of pool 0
  WETH balance of user 0
  Pool balance of user 0
  Pool balance of user2 0
  getEffectivePoolValue 0
  calcLpOut 3124870000000000000000
  getEthPriceInUSD 3124870000

  USDC balance of pool 0
  USDC balance of user 0
  WETH balance of pool 2000000000000000000
  WETH balance of user 0
  Pool balance of user 3124870000000000000000
  Pool balance of user2 3124870000000000000000
  getEffectivePoolValue 6249740000000000000000
  calcEthOut user 1000000000000000000
  calcEthOut user2 1000000000000000000
  calcLpOut 3124870000000000000000

  USDC balance of pool 0
  USDC balance of user 0
  WETH balance of pool 2000000000000000000
  WETH balance of user 0
  Pool balance of user 3124870000000000000000
  Pool balance of user2 3124870000000000000000
  getEffectivePoolValue 3124870000000000000000
  calcEthOut user 500000000000000000
  calcEthOut user2 500000000000000000
  calcLpOut 6249740000000000000000


  USDC balance of pool 0
  USDC balance of user 0
  WETH balance of pool 2000000000000000000
  WETH balance of user 0
  Pool balance of user 3124870000000000000000
  Pool balance of user2 3124870000000000000000
  getEffectivePoolValue 1000000000000
  calcEthOut user 160006656
  calcEthOut user2 160006656
  calcLpOut 19529625033800000000000000000000


  USDC balance of pool 0
  USDC balance of user 0
  WETH balance of pool 2000000000000000000
  WETH balance of user 0
  Pool balance of user 3124870000000000000000
  Pool balance of user2 3124870000000000000000
  getEffectivePoolValue 0
  calcEthOut user 0
  calcEthOut user2 0
  calcLpOut 3124870000000000000000
BVSS
Recommendation

To address this issue, modify the calcLpOut logic to gracefully handle scenarios where getEffectivePoolValue approaches or equals zero. This ensures consistent share calculations even in extreme states. Also, clearly document how the system handles near-zero getEffectivePoolValue scenarios to align user and developer expectations.

Remediation

SOLVED: The code is now using a different incentive for the described scenario by not letting the totalValueInUSD drop less than 10 USD on the calcLpOut function. Also, the client stated that:


This is an edge case that with all the mechanisms we have from the perpetual side and the overall protocol should never happen. But we thought even a solution for an isolated case of taking into account only the liquidity pool to help that case.

Remediation Hash
2543d431b0d0b2ece41225532d93dc26b7b0554f

7.3 Missing Upper Bound Validation

// Low

Description

The setswapSlippage function does not validate that the provided slippage value is less than or equal to BASIS_POINTS. If slippage is set to a value greater than BASIS_POINTS, it will result in underflows during calculations in the swapEthToUsdc and swapUsdcToEth functions. Specifically, the subtraction for minUsdcOut and minWethOut could yield negative results, causing the contract to revert.

BVSS
Recommendation

Add a validation check in the setswapSlippage function to ensure that slippage does not exceed BASIS_POINTS. For example:

function setswapSlippage(uint256 slippage) external onlyOwner {
    require(slippage <= BASIS_POINTS, "Slippage exceeds allowable range");
    swapSlippage = slippage;
}

This ensures the swapEthToUsdc and swapUsdcToEth functions operate correctly and avoids unintended underflows. By restricting slippage to a valid range, the contract's behavior remains predictable and secure.

Remediation

SOLVED: The code is now checking for the upper bounds.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.4 Allowing Zero Value Locks and Minting

// Low

Description

The lockLiquidity function does not prevent the creation of a locked liquidity entry when the calculated lpAmountOut is zero. Similarly, addLiquidity does not restrict minting when lpAmountOut is zero. These issues can result in:

  1. Operational Errors: Zero-value locks will not be correctly managed in unlockLiquidity, leading to unexpected behavior or failure.

  2. Inflation Attacks: Zero-value entries may allow unnoticed manipulation of pool shares or operational errors in the system.

  3. Rounding Issues: Small deposit values may round down to zero, introducing inaccuracies and unexpected share calculations.

Proof of Concept

    function test_halborn_zero_mint() public {
        uint256 eth_price;

        vm.startPrank(deployer);
        liquidityPool.setAddLiqAvailable(1);
        liquidityPool.setFuturesCore(address(this));
        vm.stopPrank();

        _setUnrealizedPnL(0);

        address user = address(0x1234);

        uint256 less_min_value = 300000000;

        vm.startPrank(user);
        deal(user, 10 ether);
        liquidityPool.addLiquidity{value: less_min_value}();
        vm.stopPrank();

        console.log("Pool balance of user", IERC20(address(liquidityPool)).balanceOf(user));
    }

Output:

Logs:
  Pool balance of user 0
BVSS
Recommendation

Add validation checks in both functions to ensure lpAmountOut is greater than zero before proceeding.

Remediation

SOLVED: The code is now checking for zero calculated lpAmountOut.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.5 Division by Zero Risk

// Low

Description

The calcEthOut function does not account for scenarios where totalSupply() is zero. If called under such conditions, the function will attempt to perform a division by zero, causing the contract to revert. This could lead to unexpected contract failures when calculating WETH amounts for LP tokens during initialization or under specific edge cases.

BVSS
Recommendation

Introduce a check for totalSupply() at the beginning of the function. If totalSupply() is zero, the function should return 0 to handle this edge case gracefully.

Remediation

SOLVED: The total supply function now returns 0 when there is no supply.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.6 Price Discrepancy Risk Between Chainlink Oracle and Uniswap

// Low

Description

The rebalancePool function and the swapUsdcToEthExactOutput logic rely on the Chainlink oracle for ETH/USD price data while executing swaps through the Uniswap router. Discrepancies between the Chainlink oracle price and the Uniswap price can arise due to market volatility, differences in data sources, or short-term pool manipulation. This discrepancy can result in:

  1. Inefficient rebalancing that diverges from actual pool values.

  2. Failed user transactions due to mismatches in expected and actual swap outcomes, requiring frequent adjustments of swapSlippage.

  3. Vulnerability to price manipulation attacks, especially if Chainlink prices lag behind real-time market conditions.

BVSS
Recommendation

Consider using Uniswap's prices directly for rebalancing and swaps. Use a Volume-Weighted Average Price (VWAP) over a suitable time frame to mitigate the risk of price manipulation. This approach ensures price alignment with the swap router and provides more accurate and secure operations. Specific actions include:

  1. Integrate Uniswap Price Feeds:

  2. Fetch VWAP data using Uniswap's pool observations or by querying a reliable Uniswap price oracle.

  3. Use this price as the basis for calculations in rebalancePool and swapUsdcToEthExactOutput.

  4. Implement Manipulation Safeguards:

  5. Use time-weighted average prices rather than spot prices to reduce susceptibility to flash loan or sandwich attacks.

  6. Adapt Slippage Management:

  7. Ensure setswapSlippage does not require frequent adjustments by aligning pricing mechanisms, reducing user transaction failures.

By aligning the pricing source with the swap mechanism (Uniswap), this approach ensures operational consistency, reduces risks of transaction failures, and mitigates pool manipulation threats.

Remediation

RISK ACCEPTED: The Dexodus team accepted the risk of this finding.

7.7 Inability to Update priceFeed and Static Decimal Handling

// Low

Description

The contract lacks a mechanism to update the priceFeed address, which could be problematic if the oracle becomes deprecated or unavailable. Additionally, the getEthPriceInUSD function uses hardcoded static values for decimal adjustments, which may not accommodate changes in the oracle's precision over time. This could lead to incorrect price calculations and potential loss of accuracy or functionality.

BVSS
Recommendation

Introduce a function, restricted to the owner, to allow updating the priceFeed address dynamically. Ensure the new address is validated for zero address and AggregatorV3Interface compliance. Modify getEthPriceInUSD to be decimal-agnostic by storing the price feed's decimals during initialization or update. For example:

  1. Add a uint8 public priceFeedDecimals variable to store the oracle's decimal information.

  2. During initialization or when updating the priceFeed, retrieve and store its decimals using AggregatorV3Interface.decimals().

  3. Update the getEthPriceInUSD function to use priceFeedDecimals instead of static decimal values, ensuring dynamic adjustment.

This approach enhances flexibility and accuracy while reducing reliance on external calls to the oracle for each calculation.

Remediation

SOLVED: The code is now including a setPriceFeed function and the getEthPriceInUSD will be using dynamic decimals for the oracle calculations.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.8 Missing Staleness Check in getEthPriceInUSD

// Low

Description

The getEthPriceInUSD function relies on Chainlink's price feed data but does not validate the freshness of the retrieved price. If the price feed data becomes stale (e.g., no updates for over an hour), it could lead to the use of outdated or invalid price information, introducing significant risks to calculations and decision-making within the contract.

BVSS
Recommendation

Incorporate a staleness check in the getEthPriceInUSD function. Use the timestamp provided by Chainlink's latestRoundData to ensure that the price feed data is recent.

Remediation

SOLVED: The getEthPriceInUSD function does now check for 1 hour of price window.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.9 Missing Minimal Liquidity Check in addLiquidity

// Low

Description

The addLiquidity function lacks a minimal liquidity check, indirectly relying on the calcLpOut function's behavior, which returns 0 if wethValueInUSD is less than 1e6 due to 1e18 / 1e6 rounding. While this prevents minting of fractional shares for very low ETH deposits, it does not explicitly enforce a minimal msg.value, potentially causing confusion and preventing predictable behavior for edge cases. Additionally, the lack of explicit checks may lead to misinterpretation of the function's behavior and reliance on implicit decimal handling.

BVSS
Recommendation

Introduce an explicit check for a minimal msg.value to ensure that the calculated lpAmountOut is meaningful and avoids unintentional zero-minting scenarios. The minimal value can be dynamically computed based on the current ETH price and the smallest share size.

By implementing these changes, the function avoids edge cases where users mistakenly deposit ETH with no resulting minted shares and ensures the contract's behavior is explicit and predictable.

Remediation

SOLVED: This issue was directly solved by the fact that lpAmountOut is being checked for a 0 value, those not requiring to verify the msg.value.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.10 Missing Zero Address and ERC165 Interface Checks in Initializer

// Informational

Description

The initialize function does not perform zero address validation or check for ERC165 interface compliance for the provided contract addresses (_usdc, _weth, _uniswapRouter, _priceFeed). This omission can lead to the initialization of the contract with invalid or misconfigured dependencies, potentially causing unexpected behavior or failure during runtime.

BVSS
Recommendation

In the initialize function, implement checks to ensure that the provided addresses are non-zero and validate the compliance of contracts (e.g., IERC165 support) when applicable. For example:

  • Check if each address is non-zero, using require(address != address(0), "Invalid address").

  • Validate that contracts implementing interfaces support required functions using IERC165.supportsInterface.

Adding these validations during initialization will enhance the robustness of the contract and prevent potential misconfigurations.

Remediation

SOLVED: The code now checks for 0 address.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.11 Inefficient String Comparison in lockLiquidity

// Informational

Description

The lockLiquidity function relies on string comparisons to determine the animalType tier using keccak256 hashing. This approach incurs unnecessary gas costs and increases computational complexity. Strings are less gas-efficient than alternatives like enums, which are compact and avoid the need for hashing or string processing.

BVSS
Recommendation

Replace the string-based animalType mechanism with an enum. Enums are gas-efficient, more readable, and prevent invalid inputs. Example:

  1. Define an enum for animal tiers:

   enum AnimalType { Crab, Octopus, Fish, Dolphin, Shark, Whale }
  1. Update lockLiquidity to accept AnimalType as an input:

   function lockLiquidity(uint256 months, AnimalType animalType) external payable {
       ...
       require(
           (animalType == AnimalType.Crab && ethValueInUSD >= 100e6) ||
           (animalType == AnimalType.Octopus && ethValueInUSD >= 500e6) ||
           ...
       );
       ...
   }
  1. Emit the animalType enum as an integer in the event for easier off-chain processing.

Switching to enums eliminates the gas costs associated with string hashing and improves overall contract efficiency and clarity.

Remediation

SOLVED: The code is now using enum instead of strings.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.12 Missing Bounds Check

// Informational

Description

The unlockLiquidity function does not validate whether the provided id is within the bounds of the lockedLiquidity array for the caller. If an invalid or out-of-bounds id is passed, the function will revert with a low-level error, which lacks context and can lead to confusion or unexpected failures.

BVSS
Recommendation

Add an explicit bound check to ensure the provided id is valid. For example:

require(id < lockedLiquidity[msg.sender].length, "Invalid unlock ID");

Incorporate this check before accessing lockedLiquidity[msg.sender][id]. This ensures that only valid indices are processed, preventing out-of-bounds access and enhancing the robustness of the function. The added validation also makes errors more user-friendly and informative.

Remediation

SOLVED: The code is verifying that the given id is not out of bounds.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.13 Lack of Decimal Clarity in Function Documentation

// Informational

Description

The codebase uses a mix of 18-decimal factors (e.g., WETH) and 6-decimal factors (e.g., USDC). However, function descriptions, particularly for critical functions like getEffectivePoolValue, do not explicitly clarify the decimal format of their return values. For example, getEffectivePoolValue returns values in 18 decimals, but this is not mentioned in its documentation, which could lead to misinterpretation and errors during integration or auditing.

BVSS
Recommendation

Enhance function documentation across the codebase to specify the decimal factor of input and output values clearly.

Remediation

SOLVED: The natspec of the functions was updated to include details on the decimals used.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.14 Lack of Consistent Naming for Internal Functions

// Informational

Description

The contract does not use a consistent naming convention to distinguish between internal and external functions. Internal functions (e.g., swapEthToUsdcExactOutput, swapUsdcToEthExactOutput) are not prefixed with an underscore (_), making it harder to identify their intended visibility at a glance. This could lead to confusion, accidental exposure, or misuse during contract upgrades or maintenance.

BVSS
Recommendation

Adopt a convention where all internal functions are prefixed with an underscore (_).

This naming convention enhances readability, minimizes the risk of accidental exposure, and provides a clear distinction between public-facing and internal logic, making the codebase easier to understand and maintain.

Remediation

SOLVED: The internal functions are now using _ prefix.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

7.15 Inconsistent Naming

// Informational

Description

The setswapSlippage function name does not follow standard camelCase naming conventions, which can reduce code readability and consistency. This inconsistency may lead to confusion when interacting with the function programmatically or reviewing the codebase.

Score
Recommendation

Rename the function from setswapSlippage to setSwapSlippage to adhere to standard camelCase conventions.

Remediation

SOLVED: The camel case version was used for setswapSlippage.

Remediation Hash
1f89558c1394d2c6a59238172e3e17ed50e32265

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 2024. All rights reserved.