Halborn Logo

BracketX - Bracket.fi


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/25/2024

Date of Engagement by: September 12th, 2022 - September 30th, 2022

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

0

High

0

Medium

0

Low

3

Informational

4


1. INTRODUCTION

Bracket.fi engaged Halborn to conduct a security audit on their smart contracts beginning on September 12th, 2022 and ending on September 30th, 2022. The security assessment was scoped to the smart contracts provided in the GitLab repository bracketlabs/Bracketx Halborn.

2. AUDIT SUMMARY

The team at Halborn was provided three weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contracts. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some security risks that were addressed by the Bracket.fi team.

3. TEST APPROACH & 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 audit. 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 audit:

    • 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

    • Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX)

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

    • Testnet deployment (Brownie, Remix IDE)

4. SCOPE

IN-SCOPE: The security assessment was scoped to the following smart contract:

    • Bracketx.sol

    • Config.sol

    • IPricing.sol

    • Offersx.sol

    • PricingSequencer.sol

    • StructLibx.sol

    • iBNFT.sol

Initial Commit ID:

Fixed Commit IDs:

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

3

Informational

4

Impact x Likelihood

HAL-02

HAL-03

HAL-01

HAL-04

HAL-05

HAL-06

HAL-07

Security analysisRisk levelRemediation Date
CLAIMED POLICIES CAN BE TRANSFEREDLowSolved - 09/30/2022
LACK OF DISABLEINITIALIZERS CALL TO PREVENT UNINITIALIZED CONTRACTSLowSolved - 09/30/2022
LACK OF PRICE FEED DECIMALS CHECKLowSolved - 09/30/2022
> 0 CONSUMES MORE GAS THAN != 0InformationalSolved - 10/02/2022
POSTFIX OPERATORS CONSUME MORE GAS THAN PREFIX OPERATORSInformationalSolved - 10/02/2022
INCREMENTS CAN BE UNCHECKED IN LOOPSInformationalSolved - 10/02/2022
MISSING ZERO ADDRESS CHECKInformationalSolved - 10/02/2022

8. Findings & Tech Details

8.1 CLAIMED POLICIES CAN BE TRANSFERED

// Low

Description

The transferFrom() function in the iBNFT.sol contract overrides the OpenZeppelin's ERC721 function to implement checks that prevent a policy NFT from being transferred if the token ID is 0 or above the current ID and prevent the ownership transfer of policies that have been already claimed:

iBNFT.sol

// Get a positive token price from a chainlink oracle
function transferFrom(
    address from,
    address to,
    uint256 tokenId
) whenNotPaused() nonReentrant() public override(ERC721Upgradeable, IERC721Upgradeable ) {
    require( tokenId >= 1 && tokenId <= _tokenIds.current() && !Policies[tokenId].claimed, "CLAIMED");
    super.transferFrom(from, to, tokenId);
}

\color{black} \color{white}

However, the safeTransferFrom() function is not overridden. This allows a user to easily bypass this requirement by using the safeTransferFrom() function instead of transferFrom().

  • User buys a policy and receives a NFT with token ID 1.
  • After policy expires, or the option is In-The-Money buyer claims the policy and funds are distributed accordingly.
  • User tries to transfer the policy to another user using the transferFrom() function iBNFTaddr.transferFrom(buyer1, accounts[3], 1, {'from': buyer1})
  • Because policy is claimed transaction reverts. 1.png

  • User calls the safeTransferFrom() function instead. Effectively transferring the NFT ownership and bypassing the restriction. iBNFTaddr.safeTransferFrom(buyer1, accounts[3], 1, {'from': buyer1}) 2.png

Score
Impact: 3
Likelihood: 2
Recommendation

SOLVED: The Bracket.fi team fixed the issue. Both safeTransferFrom functions, one with 3 and the other with 4 parameters, were overridden in the iBNFT contract. Moreover, the requirements were transferred to the 4 safeTransferFrom arguments, while the remaining transfer functions call it internally.

8.2 LACK OF DISABLEINITIALIZERS CALL TO PREVENT UNINITIALIZED CONTRACTS

// Low

Description

Multiple contracts are using the Initializable module from OpenZeppelin. In order to prevent leaving an implementation contract uninitialized OpenZeppelin's documentation recommends adding the _disableInitializers function in the constructor to lock the contracts automatically when they are deployed:

/**
 * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
 * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
 * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
 * through proxies.
 */
function _disableInitializers() internal virtual {
    require(!_initializing, "Initializable: contract is initializing");
    if (_initialized < type(uint8).max) {
        _initialized = type(uint8).max;
        emit Initialized(type(uint8).max);
    }
}

\color{black} \color{white}

Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue. Added the _disableInitializers() to the following contracts: Bracketxl, Config, Offersx, PricingSequencer, iBNFT.

8.3 LACK OF PRICE FEED DECIMALS CHECK

// Low

Description

The PricingSequencer contract contains the getLatestPrice() function to return the latest price from a given Chainlink price feed address. This is intended to be used with USDC pairs, which will return an 8 decimals price which is later on converted to 18 decimals by multiplying it by 1e10. However, a funder can create an offer sending an asset that would return an 18-decimals price; this would lead to the function returning a 28-decimals price.

PricingSequencer.sol

function getLatestPrice(address asset) override public view returns (uint) {
    // TODO: Add the Sequencer offline check when moving to production
    // this is not supported on the testnet

    if (checkSequencerState()) {
        // If the sequencer is down, do not perform any critical operations
        revert("L2 sequencer down: Chainlink feeds are not being updated");
    }

    //        uint80 roundID,
    //            int price,
    //            uint startedAt,
    //            uint timeStamp,
    //            uint80 answeredInRound
    AggregatorV2V3Interface priceFeed = AggregatorV2V3Interface(asset);
    (, int price, ,,) = priceFeed.latestRoundData();
    return uint256(price).mul(1e10);
}

\color{black} \color{white}

Score
Impact: 3
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue by adding code to the setOfferX function in the Offersx contract that checks that oracle uses 8 decimals before setting an offer. Although the code is commented out like oracles, it cannot be tested in local environments. Comments should be removed before deploying to production.

8.4 > 0 CONSUMES MORE GAS THAN != 0

// Informational

Description

The use of > consumes more gas than !=. There are some cases where both can be used indistinctly, such as in unsigned integers where numbers can't be negative, and as such, there is only a need to check that a number is not 0.

Code Location

Bracketx.sol

  • Line 244: assert(v.id > 0);
  • Line 376: if (v.addAvailUSDC > 0) {
  • Line 419: require((amountETH > 0 || amountUSDC > 0), "BAL");
  • Line 420: if (amountETH > 0) {
  • Line 424: if (amountUSDC > 0) {
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue by replacing > with != in the specified code.

8.5 POSTFIX OPERATORS CONSUME MORE GAS THAN PREFIX OPERATORS

// Informational

Description

The use of postfix operators i++ consume more gas than prefix operators ++i.

Code Location

Bracketx.sol

  • Line 397: for (uint tid = autoClaimPtr; tid <= maxId; tid++ ) {
  • Line 405: cnt++; Offersx.sol
  • Line 71: for (uint8 i; i < 12; i++) {
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue by replacing the postfix with prefix operators.

8.6 INCREMENTS CAN BE UNCHECKED IN LOOPS

// Informational

Description

Most of the solidity for loops use an uint256 variable counter that increments by 1 and starts at 0. These increments don't need to be checked for over/underflow because the variable will never reach the max capacity of uint256 as it would run out of gas long before that happens.

Code Location

Bracketx.sol

  • Line 397: for (uint tid = autoClaimPtr; tid <= maxId; tid++ ) {
  • Line 405: cnt++; Offersx.sol
  • Line 71: for (uint8 i; i < 12; i++) {
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue by unchecking increments in for loops.

8.7 MISSING ZERO ADDRESS CHECK

// Informational

Description

It has been detected that constructors or initializing functions of many smart contracts are missing address validation. For example:

Bracketx.sol

function initialize(
    address _ibnft,
    address _usdc,
    address _pricing,
    address _offers,
    address _config
) external initializer {
    __Ownable_init();
    __ReentrancyGuard_init();
    __Pausable_init();

    ibnft = _ibnft;
    usdc = _usdc;
    pricing = _pricing;
    offers = _offers;
    config = _config;
    autoClaimPtr = 1; // first nft is 1 not 0.
}

Every input address should be checked not to be zero, especially the ones that could lead to rendering the contract unusable, lock tokens, etc. This is considered a best practice.

Code Location

Bracketx.sol

  • Line 85-102: initialize() iBNFT.sol
  • Line 40-42: setBrkt() Offersx.sol
  • Line 39-46: initialize()
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Bracket.fi team fixed the issue by implementing non-zero address requirements in the initialized function of contracts.

9. Review Notes

Halborn performed several manual tests in the following contracts:

  • BracketX.sol
  • Config.sol
  • iBNFT.sol
  • PricingSequencer.sol
  • Offersx.sol
  • StructLibx.sol

The manual tests were focused on testing the main functions of these contracts:

  • addAvailable()
  • reduceAvailable()
  • buyPolicy()
  • claim()
  • autoClaim()
  • checkVer()
  • addVer()
  • transferFrom()
  • mintNFTinsured()
  • getOffer()
  • setOfferx()
  • getLatestPrice()
  • checkSequencerState()
  • multFactor()

No issues were found during the manual tests.

10. Automated Testing

STATIC ANALYSIS REPORT

Description

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

Slither results

BracketX.sol Config.sol iBNFT.sol Offersx.sol Offersx.sol StructLibx.sol

  • No major issues found by Slither.

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2024. All rights reserved.