Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: September 12th, 2022 - September 30th, 2022
100% of all REPORTED Findings have been addressed
All findings
7
Critical
0
High
0
Medium
0
Low
3
Informational
4
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.
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
.
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
)
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:
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 analysis | Risk level | Remediation Date |
---|---|---|
CLAIMED POLICIES CAN BE TRANSFERED | Low | Solved - 09/30/2022 |
LACK OF DISABLEINITIALIZERS CALL TO PREVENT UNINITIALIZED CONTRACTS | Low | Solved - 09/30/2022 |
LACK OF PRICE FEED DECIMALS CHECK | Low | Solved - 09/30/2022 |
> 0 CONSUMES MORE GAS THAN != 0 | Informational | Solved - 10/02/2022 |
POSTFIX OPERATORS CONSUME MORE GAS THAN PREFIX OPERATORS | Informational | Solved - 10/02/2022 |
INCREMENTS CAN BE UNCHECKED IN LOOPS | Informational | Solved - 10/02/2022 |
MISSING ZERO ADDRESS CHECK | Informational | Solved - 10/02/2022 |
// Low
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:
// 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()
.
transferFrom()
function
iBNFTaddr.transferFrom(buyer1, accounts[3], 1, {'from': buyer1})
Because policy is claimed transaction reverts.
User calls the safeTransferFrom()
function instead. Effectively transferring the NFT ownership and bypassing the restriction.
iBNFTaddr.safeTransferFrom(buyer1, accounts[3], 1, {'from': buyer1})
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.
// Low
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}
SOLVED: The Bracket.fi team
fixed the issue. Added the _disableInitializers()
to the following contracts: Bracketxl
, Config
, Offersx
, PricingSequencer
, iBNFT
.
// Low
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.
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}
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.
// Informational
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.
Bracketx.sol
assert(v.id > 0);
if (v.addAvailUSDC > 0) {
require((amountETH > 0 || amountUSDC > 0), "BAL");
if (amountETH > 0) {
if (amountUSDC > 0) {
SOLVED: The Bracket.fi team
fixed the issue by replacing > with != in the specified code.
// Informational
The use of postfix operators i++
consume more gas than prefix operators ++i
.
Bracketx.sol
for (uint tid = autoClaimPtr; tid <= maxId; tid++ ) {
cnt++;
Offersx.sol
for (uint8 i; i < 12; i++) {
SOLVED: The Bracket.fi team
fixed the issue by replacing the postfix with prefix operators.
// Informational
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.
Bracketx.sol
for (uint tid = autoClaimPtr; tid <= maxId; tid++ ) {
cnt++;
Offersx.sol
for (uint8 i; i < 12; i++) {
SOLVED: The Bracket.fi team
fixed the issue by unchecking increments in for loops.
// Informational
It has been detected that constructors or initializing functions of many smart contracts are missing address validation. For example:
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.
Bracketx.sol
initialize()
iBNFT.sol
setBrkt()
Offersx.sol
initialize()
SOLVED: The Bracket.fi team
fixed the issue by implementing non-zero address requirements in the initialized function of contracts.
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.
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.
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed