Reports

Smart Contract Security Assessment

haiVELO V2 review

haiVELO V2 allows users to use veVELO NFT to mint haiVELO token, while the V1 only allowed VELO tokens. User from haiVELO V1 can easily migrate to the new version.

6
Issues
0
C/H/M
Period
Aug 15, 2025 - Jul 16, 2025
Auditors
Adriro, HHK

Review Summary

Protocol Overview

haiVELO V2 allows users to use veVELO NFT to mint haiVELO token, while the V1 only allowed VELO tokens. User from haiVELO V1 can easily migrate to the new version.

Protocol
HAI
Timeline
Aug 15, 2025 - Jul 16, 2025
Audit Team
Adriro, HHK

Audit Overview

Scope and Resources

Scope

This audit covers one smart contract totaling approximately 120 lines of code across 1.5 days of review.

Overall Assessment

The contract is simple and well organized, the team was prompt in fixing all issues. Some concerns were raised regarding the usage of haiVELO as collateral and if the peg to VELO always holds.

Evaluation Matrix

access control
Good

No access control mechanisms are needed for this protocol's design.

mathematics
Good

The protocol uses only basic mathematical operations.

complexity
Good

The codebase is straightforward and easy to understand.

libraries
Good

The project uses battle-tested OpenZeppelin libraries.

decentralization
Low

The backing of haiVELO is stored on a Safe multisig, introducing centralization risks.

code stability
Good

The codebase remained stable throughout the audit period.

documentation
Good

NatSpec documentation is present along with existing developer documentation available on the website.

monitoring
Average

Events are emitted when needed, though a few minor gaps were identified.

testing
Good

Comprehensive test coverage is implemented.

Key Findings

Findings Summary

0
Critical
0
High
0
Medium
0
Low
5
Informational
1
Gas
I-1 Finding

I-1: V1 total supply isn't adjusted when migrating

Informational

Summary:

The implementation of migrateV1toV2() transfers V1 tokens to a burn address without adjusting the supply, while new V2 tokens are minted.

Description:

Impact:

Informational.

Recommendation:

Ensure no component depends on the total supply of the old token, and that the burn address balance is accounted for when calculating the circulating supply.

Developer Response:

Acknowledged. No fix needed.

I-2 Finding

I-2: Validate duplicates in tokens array

Informational

Summary:

Even though the implementation should fail on transferring a duplicate token ID, the depositNFTs() function could validate that there are no repeated IDs in the _tokenIds array.

Description:

Impact:

Informational.

Recommendation:

Validate token IDs are not repeated in the _tokenIds array.

    uint256 _balance = 0;
    for (uint256 i = 0; i < _tokenIds.length; i++) {
      _balance += uint256(uint128(BASE_TOKEN_NFT.locked(_tokenIds[i]).amount));
+     if (i > 0 && _tokenIds[i-1] >= _tokenIds[i]) {
+       revert WrappedTokenV2_DuplicateTokenId();
+     }
    }

Developer Response:

Fixed in https://github.com/hai-on-op/core/commit/74b2ffcfbc3e9ceda0a63fc5e21a707676bc60ca.

I-3 Finding

I-3: HaiVelo backing relies on trusted multisig custody

Informational

Description:

HaiVelo tokens maintain a 1:1 backing with VELO tokens through either pure VELO tokens or veVELO NFTs. However, during the minting process, the backing VELO tokens are transferred to the baseTokenManager, which is a Safe multisig controlled by the Hai team.

This introduces a trust assumption where any party with access to the Safe could mint additional HaiVelo tokens without proper backing, breaking the 1:1 peg.

Impact:

Informational.

Recommendation:

Store the backing VELO tokens and veVELO NFTs in an immutable smart contract that enforces the 1:1 backing mechanically, removing the need for trusted custodians.

Developer Response:

Acknowledged. We are in the process of writing a contract to handle this.

I-4 Finding

I-4: Inaccurate supply on NFT transfer callback

Informational

Summary:

The depositNFTs() function transfers the tokens before minting haiVELO, potentially leading to an incorrect supply if read from the NFT transfer callback.

Description:

Impact:

Informational.

Recommendation:

Take into account this desynchronization in future manager implementations. Another alternative would be to follow a CEI approach and mint before transferring.

Developer Response:

Fixed in https://github.com/hai-on-op/core/commit/64d57eb84b67bb088e221a6158a54740e64ef6ca.

I-5 Finding

I-5: Incorrect account argument in events

Informational

Description:

The events should notify about the user depositing or migrating, but are instead emitted with the receiver.

As an example, in the WrappedTokenV2Deposit event, the _account argument refers to the user depositing the base tokens.

20:   /**
21:    * @notice Emitted when a user deposits tokens and mints wrapped tokens
22:    * @param _account Address of the user depositing the base tokens
23:    * @param _wad Amount of tokens deposited
24:    */
25:   event WrappedTokenV2Deposit(address indexed _account, uint256 _wad);

However, the event is then emitted with the _account parameter, the receiver, which may differ from the caller.

100:     emit WrappedTokenV2Deposit(_account, _wad);

A second event should be emitting the amount of Velo per NFT locked but is emitting the sum of all Velo locked instead.

The interface:

27:  /**
28:   * @notice Emitted when a user deposits a veNFT and mints wrapped tokens
29:   * @param _account Address of the user depositing the base tokens
30:   * @param _tokenId ID of the veNFT being deposited
31:   * @param _wad Amount of locked velo in veNFT being deposited
32:   */
33:  event WrappedTokenV2NFTDeposit(address indexed _account, uint256 _tokenId, uint256 _wad);

However inside the function, it uses the sum of all NFTs locked instead of the amount per tokenId.

110: _balance += uint256(uint128(BASE_TOKEN_NFT.locked(_tokenIds[i]).amount));
...
119: emit WrappedTokenV2NFTDeposit(_account, _tokenIds[i], _balance);

Impact:

Informational.

Recommendation:

Change event arguments to msg.sender, or change the associated documentation to note that this address is the token recipient and not the originator of the action.

Developer Response:

Fixed in https://github.com/hai-on-op/core/commit/0fde26277968341029a57ac77fb8596153996312 & https://github.com/hai-on-op/core/commit/757e12b42236c4f8859853fee113f1f22780c25e & https://github.com/hai-on-op/core/commit/937ab3752c43f08ab6ae1d173b78b04d7732f7f2.

G-1 Finding

G-1: Burn address can be constant

Gas

Summary:

The BURN_ADDRESS is known at compilation time and can be defined as constant.

Description:

Impact:

Gas savings.

Recommendation:

Change the variable to constant.

Developer Response:

Fixed in https://github.com/hai-on-op/core/commit/be1e037bd0b571bcb940d789ecbbe471e073b363.

Final Remarks

The contract is simple and well organized, the team was prompt in fixing all issues. Some concerns were raised regarding the usage of haiVELO as a collateral and if the peg to VELO always hold.

Methodology

Severity Classification

Critical

Immediate threat to user funds or protocol integrity

Direct loss of funds, protocol compromise

High

Significant security risk requiring urgent attention

Potential fund loss, major functionality disruption

Medium

Important issue that should be addressed

Limited fund risk, functionality concerns

Low

Minor issue with minimal impact

Best practice violations, minor inefficiencies

Gas

Findings that improve gas efficiency

Increased transaction costs

Informational

Code quality and best practice recommendations

Reduced maintainability and readability