Halborn Logo

icon

NFTfi - Token V2 (Final Assessment) - NFTfi


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/29/2024

Date of Engagement by: February 28th, 2024 - March 4th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

0

High

0

Medium

0

Low

1

Informational

6


Introduction

NFTfi engaged Halborn to conduct a security assessment on their smart contracts beginning on February 28th and ending on March 4th. The security assessment was scoped to the smart contracts provided in the NFTfi-Genesis/eth.token GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

Assessment Summary

Halborn was provided 1 week 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 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 non-critical issues that were acknowledged and accepted by the NFTfi team.


Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the code and can quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:

  • Research into architecture and purpose.

  • Smart contract manual code review and walkthrough.

  • Graphing out functionality and contract logic/connectivity/functions (solgraph).

  • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.

  • Manual testing by custom scripts.

  • Static Analysis of security for scoped contract, and imported functions (slither).

  • Testnet deployment (Foundry).


Out-of-scope

  • External libraries and financial-related attacks.

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

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

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

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

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

2. SCOPE

Files and Repository
(a) Repository: eth.token
(b) Assessed Commit ID: bfbdeb9
(c) Items in scope:
  • contracts/BaseTokenLock.sol
  • contracts/DistributorRegistry.sol
  • contracts/DistributorTokenLock.sol
↓ Expand ↓
Out-of-Scope:
Out-of-Scope: New features/implementations after the remediation commit IDs.

3. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

1

Informational

6

Security analysisRisk levelRemediation Date
Unsafe ERC-20 operationsLowRisk Accepted - 03/19/2024
Centralization Risk for trusted ownersInformationalAcknowledged - 03/19/2024
Violation of Checks-Effects-Interactions patternInformationalAcknowledged - 03/19/2024
Missing checks for `address(0)`InformationalAcknowledged - 03/19/2024
Use of magic numbersInformationalAcknowledged - 03/19/2024
Use custom errorsInformationalAcknowledged - 03/19/2024
Outdated compiler versionInformationalAcknowledged - 03/19/2024

4. Findings & Tech Details

4.1 Unsafe ERC-20 operations

// Low

Description

Unsafe ERC-20 Operations should not be used. Some standard ERC-20 functions may not behave as expected. It is recommended to use OpenZeppelin's SafeERC20 library, and methods such as safeTransfer, safeDecreaseAllowance, safeIncreaseAllowance, forceApprove and others.


- contracts/MerkleDistributor.sol [Line: 104](contracts/MerkleDistributor.sol#L104)

	        nftfi.approve(address(distributorTokenLock), _amount);

- contracts/MerkleDistributor.sol [Line: 115](contracts/MerkleDistributor.sol#L115)

	        nftfi.transfer(owner(), _amount);
BVSS
Recommendation

It is recommended to use SafeERC20 wrappers around ERC-20 operations to enhance safety and compatibility. This can be achieved by replacing the approve method for the forceApprove or safeIncreaseAllowance methods in the context of _claim function of the MerkleDistributor smart contract (line 104).

Likewise, also for keeping consistency and best practices, it is recommended to use the safeTransfer method in drain function of MerkleDistributor contract (line 115).


Remediation Plan

RISK ACCEPTED: The NFTfi team accepted the risk related to this topic, considering the mentioned functions will not interact with external ERC-20 contracts.

References

4.2 Centralization Risk for trusted owners

// Informational

Description

Some smart contracts under analysis have owners with privileged rights to perform administrative operations and need to be trusted to not act maliciously.

- contracts/BaseTokenLock.sol [Line: 20](contracts/BaseTokenLock.sol#L20)

	abstract contract BaseTokenLock is Ownable, Pausable {

- contracts/BaseTokenLock.sol [Line: 160](contracts/BaseTokenLock.sol#L160)

	    function setTokenUtilityAccounting(address _newTokenUtilityAccounting) external onlyOwner {

- contracts/BaseTokenLock.sol [Line: 169](contracts/BaseTokenLock.sol#L169)

	    function setCooldown(uint256 _cooldown) external onlyOwner {

- contracts/BaseTokenLock.sol [Line: 181](contracts/BaseTokenLock.sol#L181)

	    function pause() external onlyOwner {

- contracts/BaseTokenLock.sol [Line: 193](contracts/BaseTokenLock.sol#L193)

	    function unpause() external onlyOwner {

- contracts/DistributorRegistry.sol [Line: 7](contracts/DistributorRegistry.sol#L7)

	contract DistributorRegistry is Ownable {

- contracts/DistributorRegistry.sol [Line: 32](contracts/DistributorRegistry.sol#L32)

	    function addDistributor(uint256 seasonNumber, address distributor) external onlyOwner {

- contracts/DistributorRegistry.sol [Line: 39](contracts/DistributorRegistry.sol#L39)

	    function removeDistributor(uint256 _seasonNumber) external onlyOwner {

- contracts/DistributorTokenLock.sol [Line: 114](contracts/DistributorTokenLock.sol#L114)

	    function setProtocolSignerAddress(address _protocolSignerAddress) external onlyOwner {

- contracts/MerkleDistributor.sol [Line: 21](contracts/MerkleDistributor.sol#L21)

	contract MerkleDistributor is Ownable, Pausable {

- contracts/MerkleDistributor.sol [Line: 114](contracts/MerkleDistributor.sol#L114)

	    function drain(uint256 _amount) public onlyOwner {

- contracts/MerkleDistributor.sol [Line: 126](contracts/MerkleDistributor.sol#L126)

	    function pause() external onlyOwner {

- contracts/MerkleDistributor.sol [Line: 138](contracts/MerkleDistributor.sol#L138)

	    function unpause() external onlyOwner {

- contracts/TokenUtilityAccounting.sol [Line: 12](contracts/TokenUtilityAccounting.sol#L12)

	contract TokenUtilityAccounting is Ownable {

- contracts/TokenUtilityAccounting.sol [Line: 107](contracts/TokenUtilityAccounting.sol#L107)

	    function addTokenLocks(address[] memory _tokenLockAddresses) external onlyOwner {

- contracts/TokenUtilityAccounting.sol [Line: 111](contracts/TokenUtilityAccounting.sol#L111)

	    function removeTokenLocks(address[] memory _tokenLockAddresses) external onlyOwner {

Score
Recommendation

It is recommended to mitigate centralization issues by implementing multi-signature mechanisms. This prevents a single entity from performing administrative and protected tasks unilaterally.


Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

4.3 Violation of Checks-Effects-Interactions pattern

// Informational

Description

The Check-Effects-Interactions in a best-practice in Solidity programming and suggests that a contract should first check all conditions and make necessary state changes (effects) before interacting with other contracts or external addresses.

In the current contract, set a violation of the CEI pattern that does not lead to reentrancy attack. However, as it's a best-practice, it is always recommended to follow the pattern, always performing external calls after checks/validations and state changes.


- contracts/BaseTokenLock.sol [Line: 150](contracts/BaseTokenLock.sol#L150)

    function _withdraw(uint256 _amount, uint256 _requestTimestamp) internal whenNotPaused {
        require(_amount <= lockedTokens[msg.sender], "withdraw amount > total");
        // cooldown checking feature can be turned off by setting it to 0
        if (cooldown > 0) {
            require(block.timestamp >= _requestTimestamp + cooldown, "cooldown not up");
            _checkAndDeleteRequest(_amount, msg.sender, _requestTimestamp);
        }
        if (cooldown == 0) {
            if (withdrawalRequestAmounts[msg.sender] > 0) {
                // if cooldown is disabled, we have to delete existing
                // cooldowns, otherwise re-using it will cause anomalies
                _checkAndDeleteRequest(_amount, msg.sender, _requestTimestamp);
            } else {
                // if there are no withdrawalRequests anymore, we have to unlock here
                tokenUtilityAccounting.unlock(msg.sender, _amount);
            }
        }

        lockedTokens[msg.sender] -= _amount;

        nftfiToken.safeTransfer(msg.sender, _amount);
        emit Withdrawn(_amount, msg.sender);
    }
Score
Recommendation

It is recommended to perform external contract calls after all state changes are performed and events are emitted.


Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

4.4 Missing checks for `address(0)`

// Informational

Description

Assigning values to address state variables without checking for address(0) is generally not recommended.


- contracts/DistributorTokenLock.sol [Line: 40](contracts/DistributorTokenLock.sol#L40)

	        protocolSignerAddress = _protocolSignerAddress;

- contracts/DistributorTokenLock.sol [Line: 115](contracts/DistributorTokenLock.sol#L115)

	        protocolSignerAddress = _protocolSignerAddress;

Score
Recommendation

It is recommended to perform input validations to avoid setting any address state variable to address(0).


Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

4.5 Use of magic numbers

// Informational

Description

During the analysis of the contracts under scope, it was specifically in the MerkleDistributor smart contract, it was identified the use of magic numbers. This can reduce code readability, as some literals might not be so obvious or intuitive.


- contracts/MerkleDistributor.sol [Line: 56](contracts/MerkleDistributor.sol#L56)

	        uint256 claimedWordIndex = _index / 256;

- contracts/MerkleDistributor.sol [Line: 57](contracts/MerkleDistributor.sol#L57)

	        uint256 claimedBitIndex = _index % 256;

- contracts/MerkleDistributor.sol [Line: 59](contracts/MerkleDistributor.sol#L59)

	        uint256 mask = (1 << claimedBitIndex);

- contracts/MerkleDistributor.sol [Line: 64](contracts/MerkleDistributor.sol#L64)

	        uint256 claimedWordIndex = _index / 256;

- contracts/MerkleDistributor.sol [Line: 65](contracts/MerkleDistributor.sol#L65)

	        uint256 claimedBitIndex = _index % 256;
Score
Recommendation

It is recommended to declare constants and use it across the code, instead of literals or "magic numbers", for improved code readability and maintenance.

For example:

uint16 someConstant = 256;


Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

4.6 Use custom errors

// Informational

Description

In Solidity smart contract development, replacing hard-coded revert message strings with the Error() syntax is an optimization strategy that can significantly reduce gas costs. Hard-coded strings, stored on the blockchain, increase the size and cost of deploying and executing contracts.

The Error() syntax allows for the definition of reusable, parameterized custom errors, leading to a more efficient use of storage and reduced gas consumption. This approach not only optimizes gas usage during deployment and interaction with the contract but also enhances code maintainability and readability by providing clearer, context-specific error information.

Score
Recommendation

It is recommended to replace hard-coded revert strings in require statements for custom errors, which can be done following the logic below.

1. Standard require statement (to be replaced):

require(condition, "Condition not met");

2. Declare the error definition to state

error ConditionNotMet();

3. As currently is not possible to use custom errors in combination with require statements, the standard syntax is:

if (!condition) revert ConditionNotMet();

More information about this topic in Official Solidity Documentation.



Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

References

4.7 Outdated compiler version

// Informational

Description

It was identified an outdated compiler version.

As from the official Solidity documentation, it is recommended to use the latest released version of Solidity.

It was identified that the contracts in the set under analysis are using solc version 0.8.19, hence, outdated, considering the current Solidity compiler (solc) version is 0.8.24.

Score
Recommendation

Update to the most recent version of Solidity (0.8.24), by changing the pragma as follows:


pragma solidity 0.8.24;


Remediation Plan

ACKNOWLEDGED: The NFTfi team acknowledged the issue.

References

5. Review Notes

update: added to the latest commit hash (post-finalization), as requested by NFTfi team on #halborn-nftfi.

6. Automated Testing

Introduction

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their ABIs and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

The security team assessed all findings identified by the Slither software, however, findings with severity Information and Optimization are not included in the below results for the sake of report readability.


slither (01)
slither (02)
slither (03)
slither (04)

The automated test findings were analyzed and classified as false-positives. Therefore, these findings were not included to the final 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.