Prepared by:
HALBORN
Last Updated 01/03/2025
Date of Engagement by: December 20th, 2024 - December 27th, 2024
100% of all REPORTED Findings have been addressed
All findings
12
Critical
0
High
0
Medium
1
Low
3
Informational
8
Halo
engaged Halborn
to conduct a security assessment on their smart contracts beginning on December 20th, 2024 and ending on December 27th, 2024. The security assessment was scoped to the smart contracts provided to Halborn
. Commit hashes and further details can be found in the Scope section of this report.
The Halo codebase in scope consist of 4 different smart contracts
The HGPBurn.sol
contract is used to allow Halo Genesis Pass owners to burn their NFTs in batches
The HaloAirdrop.sol
contract utilizes Merkle proofs to allow users to claim rewards
The HaloStakeVault.sol
contract allows users to stake HALO tokens and in return receive rewards proportional to the duration and amount of tokens they have staked
The HaloSocialMining.sol
contract utilizes Merkle proofs to allow users to claim rewards for different "sessions"
Halborn
was provided 7 days 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 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. The main ones are the following:
Consider making the ACC_PRECISION
constant variable in the HaloStakeVault.sol
contract an immutable one, and setting it in the constructor.
Use a multisig wallet for the owner address.
Consider implementing a pausing mechanism in the HaloStakeVault.sol
contract.
Add unit and E2E tests.
Halborn
performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, 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 smart contracts and can quickly identify items that do not follow security best practices.
The following phases and associated tools were used throughout the term of the assessment:
Research into architecture, purpose and use of the platform.
Smart contract manual code review and walkthrough to identify any logic issue.
Thorough assessment of safety and usage of critical Solidity variables and functions in scope that could lead to arithmetic related vulnerabilities.
Local testing with custom scripts (Foundry
).
Fork testing against main networks (Foundry
).
Static analysis of security for scoped contract, and imported functions(Slither
).
EXPLOITABILITY 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
3
Informational
8
Security analysis | Risk level | Remediation Date |
---|---|---|
HAL-08 - Decimal Precision Incompatibility in Reward Accrual with HALO Tokens | Medium | Partially Solved - 01/02/2025 |
HAL-09 - Quorum Challenges with High Staking of HALO Governance Tokens | Low | Risk Accepted - 01/02/2025 |
HAL-10 - Centralization risk | Low | Solved - 01/02/2025 |
HAL-11 - Missing pause mechanism | Low | Risk Accepted - 01/02/2025 |
HAL-02 - Custom Errors Should be Used | Informational | Acknowledged - 01/02/2025 |
HAL-07 - Owners can renounce ownership | Informational | Acknowledged - 01/02/2025 |
HAL-05 - Use of transfer() | Informational | Acknowledged - 01/02/2025 |
HAL-12 - Insufficient test coverage | Informational | Future Release - 01/02/2025 |
HAL-01 - Floating pragma | Informational | Acknowledged - 01/02/2025 |
HAL-03 - Consider Using Named Mappings | Informational | Acknowledged - 01/02/2025 |
HAL-04 - Unused import | Informational | Solved - 01/02/2025 |
HAL-06 - Use of memory instead of calldata for an unmodified function argument | Informational | Acknowledged - 01/02/2025 |
// Medium
In the HaloStakeVault.sol
contract, the stakeToken
and rewardToken
global variables are set when the contract is deployed. The ACC_PRECISION
is a constant variable with the value of 1e18. If the rewardToken
is, for example, USDC, after a certain amount of the stakeToken
which will be HALO a token with 18 decimals, is staked rewards won't accrue. Let's consider the following example:
The rewardToken
is USDC, a token with 6 decimals.
The rewardRatePerBlock
is 10e6
The totalStaked
is 10_000_001e18
When the updatePool()
function is called, and only one block has passed, the following amount will be added to the accHaloPerShare
- 10e6 * 1e18 / 10_000_001e18 = 0. The updatePool()
function is permissionless. Thus, once a certain amount of HALO tokens are staked, no more rewards will be accrued, and the users who have staked their HALO tokens won't receive the rewards that they should receive.
Consider making the ACC_PRECISION
variable an immutable one, and setting it in the constructor.
Partially Solved: The protocol team correctly followed the recommended fix, however it introduced another issue which may result in users who deposit small amounts of the stake token, to not receive any potential rewards if they withdraw too early. The protocol team has accepted this risk, and chosen to inform the users about it in its documentation.
// Low
The HALO token is a governance token, and is the token which is staked in the HaloStakeVault.sol
contract. The total supply of the token is 2_100_000_000e18
, depending on the quorum requirements if a big portion of the token is staked, quorum won't be reached in the upcoming governance proposals, as the staked tokens don't delegate the voting rights to anyone, and the HALO tokens that are deposited in the HaloStakeVault.sol
contract are not utilized in any way.
Consider delegating the voting rights associated with the HALO tokens to the address provided as the to
parameter in the stake()
function.
Risk Accepted: The protocol team has chosen not to add voting-related logic yet
// Low
If the owner address is compromised, core functionality of the protocol will be affected. For example, in the HaloAirdrop.sol
contract, a compromised owner can pause the contract indefinitely, or withdraw the tokens that are deposited and should be claimed by the users via the pause()
and emergencyWithdraw()
/ emergencyWithdrawERC20()
functions respectively.
In the HaloSocialMining.sol
contract, a compromised owner can set a new merkle root via the setSessionMerkleRoot()
function, and drain all of the rewards, or simply call the emergencyWithdraw()
/ emergencyWithdrawERC20()
in order to drain the tokens within the contract.
In the HaloStakeVault.sol
contract, a compromised owner can either increase drastically the reward per block, or make it 0 via the updateRewardRate()
function, so that stakers don't receive any rewards. It is also possible to set a rewardVault
address that doesn't have any rewards in it via the updateRewardVault()
function.
The rewardVault
contract / EOA is a single point of failure, if the reward tokens contained within it are drained, the users who staked their tokens won't receive any rewards.
Consider using multisig wallet address as the owner address.
Solved: The protocol team will set the owner to a 2/3 multiSig wallet.
// Low
The HaloStakeVault.sol
contract does not include a pause mechanism that would allow the contract owner to temporarily disable certain functions in the contract. A pause mechanism is a critical feature that can be used to prevent potential issues in the event of a bug or vulnerability being discovered in the contract. It can also be used to prevent malicious actors from exploiting the contract while a fix is being implemented.
Implement a pause mechanism in the HaloStakeVault.sol
contract that allows the contract owner to disable certain functions in the contract in case of an emergency or bug discovery.
Risk Accepted: The protocol team has accepted the risk and chosen not to implement an additional pausing mechanism.
// Informational
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.
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](https://docs.soliditylang.org/en/v0.8.24/control-structures.html#panic-via-assert-and-error-via-require).
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
// Informational
The protocol allows the current owner to renounce ownership, which will disable important functionality of the protocol such as pausing and unpausing the claiming and unlocking of user rewards within the HaloAirdrop.sol
contract, and updating the rewards per block in the HaloStakeVault.sol
contract.
Consider disallowing the owner address to be set to address(0).
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
// Informational
In the HaloSocialMining.sol
and HaloAirdrop.sol
contracts when an emergency withdraw of the native tokens is performed, the solidity build in transfer()
function is used to handle the native token transfer, it does this by forwarding a fixed amount of 2300 gas. This is dangerous for two reasons:
Gas costs of EVM instructions may change significantly during hard forks, which may previously assumed fixed gas costs. EIP 1884 as an example, broke several existing smart contracts due to a cost increase of the SLOAD instruction.
If the recipient is a contract or a multisig safe, with a receive
/fallback
function which requires >2300 gas, e.g safes that execute extra logic in the receive
/fallback
function, the transfer function will always fail for them due to out of gas errors.
Consider using the call()
method for transferring funds, as it is more flexible and handles the increase in gas cost associated with the transfer.
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
// Informational
The project lacks unit and E2E tests. It is always a good practice to implement unit tests and E2E tests, as it helps discovering vulnerabilities in the early stages of development, and later on it helps with maintaining and updating the code base.
Consider adding unit and E2E tests to the project.
Future Release: The protocol has informed us that there are tests, however they are not considered opensource as of the date of the audit.
// Informational
All contracts in scope currently use floating pragma versions ^0.8.27
which means that the code can be compiled by any compiler version that is greater than or equal to 0.8.27
, and less than 0.9.0
.
However, it is recommended that 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.
In this aspect, it is crucial to select the appropriate EVM version when it's intended to deploy the contracts on networks other than the Ethereum mainnet, which may not support these opcodes. Failure to do so could lead to unsuccessful contract deployments or transaction execution issues.
Lock the pragma version to the same version used during development and testing.
Additionally, make sure to specify the target EVM version when using Solidity versions from 0.8.20
and above if deploying to chains that may not support newly introduced opcodes. Additionally, it is crucial to stay informed about the opcode support of different chains to ensure smooth deployment and compatibility.
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
// Informational
The project is using a Solidity version greater than 0.8.18
, which supports named mappings. Using named mappings can improve the readability and maintainability of the code by making the purpose of each mapping clearer. This practice helps developers and auditors understand the mappings' intent more easily.
Consider refactoring the mappings to use named arguments, which will enhance code readability and make the purpose of each mapping more explicit.
For example, on contracts/HaloStakeVault.sol
, instead of declaring:
mapping(address => uint256) public rewardsToClaim;
It could be declared as:
mapping(address userAddress => uint256 amount) public rewardsToClaim;
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
// Informational
The HGPBurn.sol
contract imports the import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
however it doesn't use it.
Consider removing the unused import, the import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
is imported in the IHaloWalletGenesisPass.sol.
Solved: The protocol has removed the unused import.
// Informational
In the HaloAirdop.sol
contract in the isValid()
function, the memory keyword is used for a parameter which is not modified instead of the calldata keyword.
Consider using the calldata keyword instead of the memory for function arguments which are not modified.
Acknowledged: The protocol team has acknowledged the issue, and chosen not to make changes.
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 contract. 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, most of the findings are not included in the below results for the sake of report readability.
The findings obtained as a result of the Slither scan were reviewed, and the majority were not included in the report because they were determined as false positives.
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