Prepared by:
HALBORN
Last Updated 08/08/2024
Date of Engagement by: April 25th, 2024 - June 6th, 2024
100% of all REPORTED Findings have been addressed
All findings
10
Critical
0
High
0
Medium
1
Low
1
Informational
8
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.
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
.
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
).
External libraries and financial-related attacks.
New features/implementations after/with the remediation commit IDs.
Changes that occur outside the scope of PRs.
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
0
Medium
1
Low
1
Informational
8
Security analysis | Risk level | Remediation Date |
---|---|---|
Sandwich Attack in convertTokens Function | Medium | Risk Accepted |
Lack of Upper Bound on Execution Fee | Low | Solved - 08/01/2024 |
Accounting Errors with Deflationary Tokens | Informational | Acknowledged |
Inability to Remove Tokens from Pool Leading to Maximum Asset Limit | Informational | Acknowledged |
Use Ownable2StepUpgradeable for Secure Ownership Transfer | Informational | Acknowledged |
Unlocked Pragma | Informational | Solved - 06/24/2024 |
Incomplete Implementation of getAssetAum Function | Informational | Solved - 06/25/2024 |
Insufficient Gas Limit for ETH Transfer in ETHUnwrapper Contract | Informational | Solved - 06/24/2024 |
Typo in Comment for Liquidity Execution Fee Setting | Informational | Solved - 08/01/2024 |
Commented Out Expiration Field in LiquidityOrder Struct | Informational | Acknowledged |
// Medium
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;
}
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.
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.
RISK ACCEPTED: The LPBase team accepted the risk of the issue.
// Low
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.
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.
SOLVED: The LPBase team solved the issue.
// Informational
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.
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.
ACKNOWLEDGED: The LPBase team acknowledged this finding.
// Informational
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);
}
Consider fixing the logic on the addAsset
function.
ACKNOWLEDGED: The LPBase team acknowledged this finding.
// Informational
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 {}
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.
ACKNOWLEDGED: The LPBase team acknowledged this finding.
// Informational
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;
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;
SOLVED: The LPBase team solved the issue by locking pragma.
// Informational
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.
To address this issue and complete the implementation of the getAssetAum
function, the following steps should be taken:
Uncomment the code block below the return 0
statement to enable the actual AUM calculation logic.
SOLVED: The LPBase team solved the issue by applying the suggested recommendation.
// Informational
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.
Replace the fixed gas limit of 2400 with a higher value that is sufficient for Gnosis Safe contracts to process the ETH transfer successfully.
SOLVED: The LPBase team solved the issue by applying the suggested recommendation.
// Informational
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();
}
_;
}
Correct the spelling of "liquidity" in the comment.
SOLVED: The LPBase team solved the issue.
// Informational
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.
1. Re-enable the expiresAt
field in the LiquidityOrder struct:
struct LiquidityOrder {
// ... other fields ...
uint256 expiresAt;
uint256 executionFee;
}
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed