Core V2 - IP.World


Prepared by:

Halborn Logo

HALBORN

Last Updated 07/25/2025

Date of Engagement: July 1st, 2025 - July 7th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

14

Critical

1

High

1

Medium

4

Low

4

Informational

4


1. Introduction

IPWorld engaged Halborn to conduct a security assessment of their smart contracts starting at July 1st, 2025 and ending on July 7th, 2025. The security assessment was scoped to the smart contracts provided in the ipdotworld/core Github repository provided to Halborn. Further details can be found in the Scope section of this report.

2. Assessment Summary

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


The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were mostly addressed by the IPWorld team. The main ones were the following: 

    • Initialize nextTick prior to entering the harvest loop to prevent startTick corruption and ensure proper fee collection.

    • Create vesting schedule immediately when prerequisites are met, independent of WETH collection to prevent permanent blocking.

    • Clamp tick bounds within repositionBidWall() to prevent TickMath reverts and harvest denial-of-service attacks.

    • Accept user-defined deadline parameters in liquidity operations to restore slippage protection against MEV attacks.

    • Rename V2 initializer to initializeV2 and protect with reinitializer(2) modifier to enable proper proxy upgrades.

    • Remove base initializers from reinitializer functions to prevent "already initialized" reverts during upgrades.

    • Use call() without gas limits for ETH transfers to ensure compatibility with smart contract wallets.

    • Add missing safety guards to claimToken() including array length validation and SafeERC20 usage.

    • Add fee-share sum validation in constructor to prevent treasury calculation underflows and harvest reverts.

3. Caveats

The Operator role has extensive control over both data and funds within the IPWorld system. Since it is assumed to be a trusted role, risks and findings specifically tied to it were not included in this report. However, Halborn strongly recommends that the IPWorld team handle all private keys, including the Operator key, with the highest level of security and operational caution.

4. Test Approach and Methodology

Halborn employed a combination of manual, semi-automated, and automated security testing to balance efficiency, timeliness, practicality, and accuracy within the scope of this assessment. Manual testing is essential for uncovering flaws in logic, process, and implementation, while automated techniques enhance code coverage and quickly identify deviations from security best practices. The following phases and tools were utilized throughout the assessment:

    • Research into the architecture and purpose of the smart contracts.

    • Manual code review and walkthrough of the smart contracts.

    • Manual assessment of critical Solidity variables and functions to identify potential vulnerability classes.

    • Manual testing using custom scripts.

    • Static security analysis of the scoped contracts and imported functions using Slither.

    • Local deployment and testing with Foundry.


5. 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.

5.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:
EXPLOITABILITY 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

5.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}

5.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

6. SCOPE

Files and Repository
(a) Repository: core
(b) Assessed Commit ID: 62c2f9f
(c) Items in scope:
  • src/IPworld.Sol
  • src/IPOwnerVault.sol
  • src/IPWorldManager.sol
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

1

High

1

Medium

4

Low

4

Informational

4

Security analysisRisk levelRemediation Date
Incorrect Tick-Segment Search Yields Zero-Fee HarvestsCriticalSolved - 07/10/2025
Zero-WETH Harvest & Single-Range Deployment Can Permanently Block VestingHighSolved - 07/10/2025
Extreme-Tick Bid-Wall Repositioning Can Cause harvest DoSMediumSolved - 07/12/2025
Uncallable Initializer in IPOwnerVaultMediumSolved - 07/10/2025
Repeating Base Initializers in Re-Initializer Will RevertMediumSolved - 07/10/2025
Harvest fails for old tokens without repositionBidWall() functionMediumSolved - 07/22/2025
Hard-Coded deadline Nullifies Slippage ProtectionLowSolved - 07/10/2025
Operator-Only claimToken() Is UnusedLowSolved - 07/10/2025
Fixed-Gas ETH Transfer May Revert for Smart-Contract WalletsLowSolved - 07/10/2025
Fee-Share Sum Not BoundedLowSolved - 07/10/2025
claimIp() Lacks Basic ValidationInformationalSolved - 07/23/2025
setExpectedSigner Accepts Zero AddressInformationalSolved - 07/10/2025
Unchecked Increments Could Save GasInformationalAcknowledged - 07/10/2025
TODO Markers Indicating Incomplete FeaturesInformationalSolved - 07/23/2025

8. Findings & Tech Details

8.1 Incorrect Tick-Segment Search Yields Zero-Fee Harvests

//

Critical

Description

IPWorld::harvest() attempts to locate which tick interval currently contains the pool price so it can burn/collect the proper liquidity segment. The loop that computes startTick and nextTick overwrites startTick with an uninitialised nextTick (0) on its first iteration, corrupting the bounds:

int24 startTick = startTickList[0];
int24 nextTick;
uint256 i;
for (i = 1; i <= length; ++i) {
    startTick = nextTick;                 // nextTick is 0 on first pass
    nextTick = (i == length) ? MAX_TICK : startTickList[i];
    if (startTick < currentTick && currentTick < nextTick) break;
}

With startTick reset to 0, the function later calls burn() / collect() on a range (e.g., -887 220 → 120 000) that does not match the stored position (120 000 → 144 000). This logic harvests from an empty range, returning (0,0) and starving the protocol of fees.


Proof of Concept

Run forge test --mt test_Vesting_CreatedVerified -vvv.


We can observe that the harvest is 0.
BVSS
Recommendation

Initialize nextTick prior to entering the loop and assign variable values in the proper sequence:

int24 nextTick = startTickList[0];
int24 startTick;
for (uint256 i = 1; i <= length; ++i) {
    startTick = nextTick;
    nextTick  = (i == length) ? MAX_TICK : startTickList[i];
    if (startTick < currentTick && currentTick < nextTick) break;
}

Remediation Comment

SOLVED: The IPWorld team has successfully implemented the recommended mitigation measures.

Remediation Hash

8.2 Zero-WETH Harvest & Single-Range Deployment Can Permanently Block Vesting

//

High

Description

IPWorld.harvest() initializes the IP owner vesting schedule only after it has collected at least one wei of WETH.

if (wethAmount != 0) {
    …
    if (recipient != address(0) && !IIPOwnerVault(ownerVault).vesting(token).isSet) {
        IIPOwnerVault(ownerVault).createVestingOnTokenDeploy(token);
    }
}

Exploit Scenario:

  • No fees have accrued yet — immediately after createIpToken() mints liquidity, fee counters are at zero.

  • Permissionless harvest — anyone can call harvest(token); the IP owner does not need to be the first caller.

  • The caller invokes harvest before any swap occurs, resulting in wethAmount == 0 and bypassing the vesting process.

  • Single-range deployment (optional) — if the token was deployed with a single tick range, this zero-fee harvest also removes all liquidity from the pool and burns a portion of the IP tokens.


Impact:

  • The vesting schedule is never established; the IP owner cannot withdraw their allocation (vesting(token).isSet == false).

  • Protocol revenue sharing (distributeOwdAmount) is delayed.

  • In the case of a single tick range, the situation is permanently blocked because no further WETH fees can accrue without liquidity.


Proof of Concept

Run forge test --mt test_Vesting_CreatedVerified -vvv, as in here we are receiving no harvest. We can observe that the vesting schedule is not created:



BVSS
Recommendation

Create the vesting schedule promptly once both prerequisites are satisfied:

  • Tokens are stored in the vault.

  • An IP recipient has been assigned.

address recipient = _ipaRecipient[ipaId];
if (recipient != address(0) && !IIPOwnerVault(ownerVault).vesting(token).isSet) {
    IIPOwnerVault(ownerVault).createVestingOnTokenDeploy(token);
}

if (wethAmount != 0) {
    … // fee distribution logic
}

Remediation Comment

SOLVED: The suggested mitigation has been implemented by the IPWorld team.

Remediation Hash

8.3 Extreme-Tick Bid-Wall Repositioning Can Cause harvest DoS

//

Medium

Description

When IPWorld.harvest() completes fee collection, it forwards a portion of the WETH to the IP-token and calls repositionBidWall():

uint256 bidWallAmount = wethAmount * bidWallShare / PRECISION;
IERC20(_weth).transfer(address(token), bidWallAmount);
IIPToken(token).repositionBidWall();

The IPToken.repositionBidWall() function removes the existing "bid-wall" liquidity position and mints a new one one tick away from the current price. The following describes the two symmetric failure scenarios:


  • Left boundary (−MAX_TICK): occurs when the IP-token is token0 (nativeIsZero == false).

_collectLiquidity(pool, bidWallTickLower, nativeIsZero);
...
int24 baseTick   = currentTick - 1;           // = −887 221
newTickUpper     = _validTick(baseTick, true); // clamps to −MAX_TICK
newTickLower     = newTickUpper - TICK_SPACING; // −887 280   < MIN_TICK
_addLiquidity(... newTickLower ...)            // ↯ TickMath.revert("T")

  • Right boundary (+MAX_TICK): occurs when WETH is token0 (nativeIsZero == true).

_collectLiquidity(pool, bidWallTickLower, nativeIsZero);
...
int24 baseTick   = currentTick + 1;           // = 887 221
newTickLower     = _validTick(baseTick, false); // clamps to +MAX_TICK
newTickUpper     = newTickLower + TICK_SPACING; // 887 280  > MAX_TICK
_addLiquidity(... newTickUpper ...)            // ↯ TickMath.revert("T")

In cases where newTickLower underflows below TickMath.MIN_TICK (−887,272), or newTickUpper overflows above TickMath.MAX_TICK (887,220), both scenarios cause TickMath to revert with the generic "T()" error, effectively rendering harvest() inoperable.


Impact:

  • This is an edge case but can result in a repeatedly-triggerable denial-of-service for IPWorld.harvest() on affected tokens.

  • Protocol fee distribution, bid-wall refresh, treasury income, and IP-owner vesting are all interrupted.

  • The attack cost is minimal once the attacker possesses enough tokens to manipulate the thin one-sided pool.


Proof of Concept

Add the following test case to test/IPWorld.t.sol:

 function test_BidWall_DoS_Revert() public {
        vm.prank(address(operator));
        (, address tokenAddr) =
            ipWorld.createIpToken(alice, "CHILL", "CHILL", address(0), startTickList, allocationList);

        IPToken ipToken = IPToken(tokenAddr);

        // 2. Fund the token with a small amount of WETH to bypass the 0-balance guard.
        //    This mimics the WETH that IPWorld would send during harvest.
        weth.deposit{value: 1 ether}();
        weth.transfer(tokenAddr, 1 ether);

        // 3. Calling `repositionBidWall()` now must revert with Uniswap's 'T' error
        //    because the computed `newTickLower` underflows below MIN_TICK.
        vm.expectRevert(bytes("T()"));
        ipToken.repositionBidWall();
    }

BVSS
Recommendation

Clamp tick bounds within repositionBidWall() (and _collectLiquidity) to ensure:

  • If newTickLower would be below MIN_TICK, it is set to MIN_TICK.

  • If newTickUpper would be above MAX_TICK, it is set to MAX_TICK.


Remediation Comment

SOLVED: The IPWorld team implemented boundary checks after tick calculation. The system now skips repositioning when near tick boundaries while maintaining all essential protocol functions, including fee collection, distribution, token burning, and treasury payments, which continue to operate normally. This approach creates a system where the bid wall temporarily halts repositioning at extreme price levels that are economically costly to reach and sustain, rather than disrupting the entire protocol. Repositioning automatically resumes when market conditions normalize.

Remediation Hash

8.4 Uncallable Initializer in IPOwnerVault

//

Medium

Description

The IPOwnerVault::initialize function in V2 retains the initializer modifier from version 1.

function initialize(address initialOwner) public initializer {
    __Ownable_init(initialOwner);
    __UUPSUpgradeable_init();
}

Since the proxy has already been initialized using version 1, this call will revert.

BVSS
Recommendation

Rename it to initializeV2 and protect it using reinitializer(2).

Remediation Comment

SOLVED: The IPWorld team has implemented the recommended mitigation measures.

Remediation Hash

8.5 Repeating Base Initializers in Re-Initializer Will Revert

//

Medium

Description

IPWorld.sol defines an upgrade hook:

function initialize(address initialOwner) public reinitializer(2) {
    __UUPSUpgradeable_init();     
    __Ownable_init(initialOwner); 
}

Both __UUPSUpgradeable_init() and __Ownable_init() are tagged with initializer (version 1). After the proxy is deployed, version 1 is already consumed. Calling them again from inside a reinitializer(2) will therefore revert with "contract is already initialized". Similar issue exists in IPOwnerVault.sol.


BVSS
Recommendation

Remove the base initializers and only set new variables:

function initializeV2(..) public reinitializer(2) {
    // no __UUPSUpgradeable_init or __Ownable_init here
    newStateVar = …;
}

Remediation Comment

SOLVED: The suggested mitigation was implemented.

Remediation Hash

8.6 Harvest fails for old tokens without repositionBidWall() function

//

Medium

Description

After upgrading the IPWorld contract, the harvest() function fails when called on tokens deployed with older versions of IPToken that lack the repositionBidWall() function. This introduces a critical backward compatibility issue, preventing successful fee collection and distribution for existing tokens.

function harvest(address token) external {
    // ... fee collection logic ...
    
    if (wethAmount != 0) {
        uint256 bidWallAmount = wethAmount * bidWallShare / PRECISION;
        uint256 ipOwnerAmount = wethAmount * ipOwnerShare / PRECISION;
        IERC20(_weth).transfer(address(token), bidWallAmount);
        IIPToken(token).repositionBidWall(); 
        
        // ... rest of distribution logic
    }
}

Impact:

  • Failure to harvest fees from legacy tokens

  • Blocked distribution of LP fees to IP owners and stakers

  • Potential accumulation of unharvested fees within pools


BVSS
Recommendation

Implement a fallback mechanism that:

  1. Attempts to retrieve the pool address via the liquidityPool() function.

  2. If this attempt fails, calculates the pool address using the Uniswap V3 Factory.

  3. For legacy tokens, skips the buyback process and directs the buyback funds to the treasury instead.


Remediation Comment

SOLVED: The above fix was implemented by IPWorld team.

Remediation Hash

8.7 Hard-Coded deadline Nullifies Slippage Protection

//

Low

Description

The IPWorldLPManager forwards liquidity operations to the NonfungiblePositionManager. For both mint() and burn() functions, it sets the deadline parameter to block.timestamp instead of accepting it from the user:

deadline: block.timestamp            

Since the call is executed within the same transaction, the check require(block.timestamp <= deadline) within the periphery contract always passes, even if the user's transaction remains in the mempool for minutes or hours. This hard-coded value effectively nullifies the purpose of the deadline parameter, leaving liquidity providers vulnerable to price fluctuations and MEV exploitation while their transaction is pending.


BVSS
Recommendation

Accept a uint256 deadline parameter from the caller and pass it unchanged to the position-manager struct:

function mint(..., uint256 deadline) external payable returns (...) {
    require(block.timestamp <= deadline, "deadline passed");
    INonfungiblePositionManager.MintParams memory params = … { deadline: deadline };
    ...
}

Similarly, this applies to the burn() helper function.

Remediation Comment

SOLVED: The IPWorld team has implemented the recommended mitigation measures.

Remediation Hash

8.8 Operator-Only claimToken() Is Unused

//

Low

Description

IPWorld::claimToken() permits the Operator to transfer any ERC-20 tokens held by the IPWorld contract to an arbitrary set of recipients:

function claimToken(address token, address[] calldata addressList, uint256[] calldata amountList)
    external
    onlyOperator
{
    …
    if (amount > 0) IERC20(token).transfer(recipient, amount);
}

However, this function is never invoked by the designated Operator contract.


Additional vulnerabilities within claimToken() increase the security risk:

  • The function does not enforce that addressList.length == amountList.length.

  • It ignores the return value of IERC20.transfer, potentially losing funds when interacting with non-standard ERC-20 tokens.

  • No events are emitted, which hampers off-chain monitoring and auditing.


BVSS
Recommendation
  1. Clarify responsibility:

    • If token recovery is meant to be manual, restrict the function to onlyOwner (or remove it entirely) and execute via multisig.

    • If it must remain operator-controlled, expose a wrapper in Operator that is protected by off-chain signatures.

  2. Add the missing safety guards:

    • require(addressList.length == amountList.length);

    • Emit an event (e.g., TokenClaimed(token, recipient, amount)).

    • Use SafeERC20.safeTransfer to handle non-standard tokens.


Remediation Comment

SOLVED:

This finding was fixed with following comment from IPWorld team:

  • Regarding "unused function": The claimToken() function will be called by an EOA (Externally Owned Account) Operator, not by the Operator contract. This is an intentional design decision for manual token distribution.

  • Implemented fix: Added array length validation to prevent out-of-bounds access

    • Added new error IPWorld_InvalidArgument for invalid arguments

    • Added validation: if (length \!= amountList.length) revert Errors.IPWorld_InvalidArgument();

    • This ensures addressList and amountList have matching lengths


Remediation Hash

8.9 Fixed-Gas ETH Transfer May Revert for Smart-Contract Wallets

//

Low

Description

Operator::_transferETH() sends ETH to a recipient using a hard-coded gas stipend of 30,000:

(bool success,) = to.call{value: value, gas: 30_000}(new bytes(0));
if (!success) revert();

Many smart contract wallets (e.g., Gnosis Safe) and on-chain interactions require more than 30,000 gas to execute their fallback functions. When the recipient is such a contract, the call reverts, preventing users from receiving refunds or proceeds after createIpTokenWithSig and other processes that rely on _transferETH.


Consequently, certain wallet types (e.g., safes, paymasters, proxy wallets) are unable to interact with the protocol because refund transactions fail.


BVSS
Recommendation
  1. Use Address.sendValue (forwards all available gas and reverts on failure) or an unconstrained call{value: value}.

  2. Alternatively, allow the caller to withdraw unclaimed ETH manually.


Remediation Comment

SOLVED: Removed the hardcoded gas limit of 30,000 from the _transferETH function and replaced it with a call to to.call{value: value}("")


Remediation Hash

8.10 Fee-Share Sum Not Bounded

//

Low

Description

In IPWorld::constructor, the variables burnShare, ipOwnerShare, and bidWallShare are only validated individually through range checks. However, if the sum of ipOwnerShare and bidWallShare exceeds PRECISION (1,000,000), it causes an underflow in the treasury payout calculation, resulting in every harvest() transaction reverting.


(success,) = treasury.call{value: wethAmount - ipOwnerAmount - bidWallAmount}("");

BVSS
Recommendation

Include the following check in the constructor:

require(ipOwnerShare + bidWallShare <= PRECISION, "Share overflow");

Remediation Comment

SOLVED: The IPWorld team has implemented the recommended mitigation measures.

Remediation Hash

8.11 claimIp() Lacks Basic Validation

//

Informational

Description

The IPWorld::claimIp() function allows an operator to overwrite the existing recipient for any IP without checking whether the IP was previously claimed or that the caller is the current owner.

BVSS
Recommendation

Add basic safeguards such as:

require(_ipaRecipient[ipaId] == address(0), "IP already claimed");

or implement a two-step transfer process, requiring the current recipient to approve any changes.

Remediation Comment

SOLVED: Implemented a two-step transfer process requiring approval from the current owner.

Remediation Hash

8.12 setExpectedSigner Accepts Zero Address

//

Informational

Description

In Operator::setExpectedSigner, setting signer to address(0) blocks all future EIP-712 operations.

function setExpectedSigner(address expectedSigner_) external onlyOwner {
    expectedSigner = expectedSigner_;
}

BVSS
Recommendation

Add a check for address(0):

require(expectedSigner_ != address(0), "zero signer");

Remediation Comment

SOLVED: The setExpectedSigner function now performs proper validation to prevent setting the zero address.


Remediation Hash

8.13 Unchecked Increments Could Save Gas

//

Informational

Description

In IPWorld, loop counters utilize checked arithmetic, although overflow cannot occur in this context.

for (uint256 i = 1; i <= length; ++i) { ... }

BVSS
Recommendation

Use the unchecked block for for loop increments in IPWorld.sol:

for (uint256 i = 1; i <= length; ) {
    ...
    unchecked { ++i; }
}

Remediation Comment

ACKNOWLEDGED: The IPWorld team acknowledged the finding with following comment - "While the suggestion is technically valid, the trade-off between code clarity and minimal gas savings doesn't justify the change at this time."

8.14 TODO Markers Indicating Incomplete Features

//

Informational

Description

Several contracts still contain // TODO comments indicating incomplete logic. These placeholders range from missing event emissions to generic revert() statements lacking specific error codes. Leaving such TODOs in deployed code reduces transparency, disrupts off-chain accounting, and can result in vague or misleading failure messages.

BVSS
Recommendation

Complete the implementation or removal of all TODO items prior to deployment:

  • IPToken.sol::storyHuntV3MintCallback

  • IPWorldLPManager.sol::whitelistToken

  • IPWorldLPManager.sol::burn

  • IPOwnerVault.sol::constructor

  • IPWorld.sol::harvest


Remediation Comment

SOLVED: The issues marked with TODO placeholders have been addressed and resolved.

Remediation Hash

9. Automated Testing

Halborn employed automated testing techniques to improve coverage in specific areas of the scope's smart contracts. One of the tools utilized was Slither, a Solidity static analysis framework. After verifying that the smart contracts in the repository could be compiled correctly into their ABIs and binary formats, Slither was executed against the contracts. This tool performs static analysis to verify mathematical relationships between Solidity variables, enabling the detection of invalid or inconsistent API usage throughout the entire codebase.


All issues detected by Slither were determined to be false positives and were therefore not included in the issue list presented in this report.

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