Prepared by:
HALBORN
Last Updated 11/25/2024
Date of Engagement by: November 18th, 2024 - November 20th, 2024
100% of all REPORTED Findings have been addressed
All findings
15
Critical
0
High
1
Medium
1
Low
7
Informational
6
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.
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.
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.
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
1
Medium
1
Low
7
Informational
6
Security analysis | Risk level | Remediation Date |
---|---|---|
Incorrect Interface Used for Uniswap Interactions on Base Network | High | Solved - 11/21/2024 |
Edge Case Leading to Inconsistent LP Share Minting | Medium | Solved - 11/22/2024 |
Missing Upper Bound Validation | Low | Solved - 11/21/2024 |
Allowing Zero Value Locks and Minting | Low | Solved - 11/21/2024 |
Division by Zero Risk | Low | Solved - 11/21/2024 |
Price Discrepancy Risk Between Chainlink Oracle and Uniswap | Low | Risk Accepted - 11/21/2024 |
Inability to Update priceFeed and Static Decimal Handling | Low | Solved - 11/21/2024 |
Missing Staleness Check in getEthPriceInUSD | Low | Solved - 11/21/2024 |
Missing Minimal Liquidity Check in addLiquidity | Low | Solved - 11/21/2024 |
Missing Zero Address and ERC165 Interface Checks in Initializer | Informational | Solved - 11/21/2024 |
Inefficient String Comparison in lockLiquidity | Informational | Solved - 11/21/2024 |
Missing Bounds Check | Informational | Solved - 11/21/2024 |
Lack of Decimal Clarity in Function Documentation | Informational | Solved - 11/21/2024 |
Lack of Consistent Naming for Internal Functions | Informational | Solved - 11/21/2024 |
Inconsistent Naming | Informational | Solved - 11/21/2024 |
// High
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;
}
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();
}
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.
SOLVED: The code is now using the IV3SwapRouter
interface that matches the one deployed in Base.
// Medium
When getEffectivePoolValue
approaches zero due to unrealized profits held by traders, the LP share calculations in calcLpOut
exhibit inconsistent behavior. Specifically:
Inflated LP Shares: As getEffectivePoolValue
approaches zero (but remains slightly positive), the calcLpOut
function mints excessively high LP shares.
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.
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
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.
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.
// Low
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.
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.
SOLVED: The code is now checking for the upper bounds.
// Low
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:
Operational Errors: Zero-value locks will not be correctly managed in unlockLiquidity
, leading to unexpected behavior or failure.
Inflation Attacks: Zero-value entries may allow unnoticed manipulation of pool shares or operational errors in the system.
Rounding Issues: Small deposit values may round down to zero, introducing inaccuracies and unexpected share calculations.
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
Add validation checks in both functions to ensure lpAmountOut
is greater than zero before proceeding.
SOLVED: The code is now checking for zero calculated lpAmountOut
.
// Low
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.
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.
SOLVED: The total supply function now returns 0 when there is no supply.
// Low
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:
Inefficient rebalancing that diverges from actual pool values.
Failed user transactions due to mismatches in expected and actual swap outcomes, requiring frequent adjustments of swapSlippage
.
Vulnerability to price manipulation attacks, especially if Chainlink prices lag behind real-time market conditions.
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:
Integrate Uniswap Price Feeds:
Fetch VWAP data using Uniswap's pool observations or by querying a reliable Uniswap price oracle.
Use this price as the basis for calculations in rebalancePool
and swapUsdcToEthExactOutput
.
Implement Manipulation Safeguards:
Use time-weighted average prices rather than spot prices to reduce susceptibility to flash loan or sandwich attacks.
Adapt Slippage Management:
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.
RISK ACCEPTED: The Dexodus team accepted the risk of this finding.
// Low
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.
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:
Add a uint8 public priceFeedDecimals
variable to store the oracle's decimal information.
During initialization or when updating the priceFeed
, retrieve and store its decimals using AggregatorV3Interface.decimals()
.
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.
SOLVED: The code is now including a setPriceFeed
function and the getEthPriceInUSD
will be using dynamic decimals for the oracle calculations.
// Low
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.
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.
SOLVED: The getEthPriceInUSD
function does now check for 1 hour of price window.
// Low
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.
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.
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
.
// Informational
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.
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.
SOLVED: The code now checks for 0 address.
// Informational
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.
Replace the string-based animalType
mechanism with an enum. Enums are gas-efficient, more readable, and prevent invalid inputs. Example:
Define an enum
for animal tiers:
enum AnimalType { Crab, Octopus, Fish, Dolphin, Shark, Whale }
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) ||
...
);
...
}
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.
SOLVED: The code is now using enum instead of strings.
// Informational
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.
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.
SOLVED: The code is verifying that the given id is not out of bounds.
// Informational
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.
Enhance function documentation across the codebase to specify the decimal factor of input and output values clearly.
SOLVED: The natspec of the functions was updated to include details on the decimals used.
// Informational
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.
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.
SOLVED: The internal functions are now using _
prefix.
// Informational
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.
Rename the function from setswapSlippage
to setSwapSlippage
to adhere to standard camelCase
conventions.
SOLVED: The camel case version was used for setswapSlippage
.
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed