Smart Contracts

Technical reference for NFTH smart contracts.

Vault Naming Convention

Following NFTX V2 naming guidelines, vault token symbols should be:

  • Uppercase and alphanumeric
  • Less than 7 characters (for Twitter cashtag compatibility)
  • No "v" prefix - Use THC not vTHC
  • Singular form - Use PUNK not PUNKS

Examples: THC, VHYPUR, VHYPIO, PUNK, BAYC

Contract Addresses

HyperEVM Mainnet (Chain ID: 999)

Coming soon

HyperEVM Testnet (Chain ID: 998)

Core Contracts (Audited NFTX V2):

ContractAddress
NFTXVaultFactoryUpgradeable0x9bFF7c7a0E9799d0494F02551077069FBEa2Ff6F
NFTXVaultUpgradeable (impl)0x5c870C830D23dF7017d9ecE0Fa4b6119F489219B
NFTXSimpleFeeDistributor0x732A312B15250F7B43321f6e8A23212840ce4568

Periphery Contracts (Non-Audited):

ContractAddress
NFTXVaultRouter0xa1d5F4c37a71D9021913dB1b57102CdE642797e5
NFTXVaultWrapperDeployed per vault via UI

Mock NFT Collections (Testnet Only):

CollectionAddressMax Supply
Tiny Hyper Cats0x2bf1e6d6776cA15558B71eB5fa8cA37CC9Dd513f2222
Hypurr0x13e992622141bFa177ce42f5C9252DEa08891b554600
Hypio0x87Db4A9f872Fe496166ea1b518e2c6c3D264cff75555

RPC: https://rpc.hyperliquid-testnet.xyz/evm

Core Contracts

NFTXVaultFactoryUpgradeable

The factory contract that deploys and manages vaults.

Key Functions:

// Create a new vault
function createVault(
    string calldata name,
    string calldata symbol,
    address assetAddress,
    bool is1155,
    bool allowAllItems
) external returns (uint256 vaultId);

// Get all vaults for an NFT collection
function vaultsForAsset(address asset)
    external view returns (address[] memory);

// Get vault address by ID
function vault(uint256 vaultId)
    external view returns (address);

NFTXVaultUpgradeable

Individual vault contracts that hold NFTs and issue vTokens.

Key Functions:

// Deposit NFTs, receive vTokens
function mint(
    uint256[] calldata tokenIds,
    uint256[] calldata amounts
) external returns (uint256 vTokensMinted);

// Burn vTokens, receive random NFTs
function redeem(
    uint256 amount,
    uint256[] calldata specificIds
) external returns (uint256[] memory redeemedIds);

// Burn vTokens, receive specific NFTs (1% fee)
function redeemTo(
    uint256 amount,
    uint256[] calldata specificIds,
    address to
) external returns (uint256[] memory redeemedIds);

NFTXEligibilityManager

Manages eligibility modules that determine which NFTs can be deposited.

Eligibility Types:

  • NFTXListEligibility - Whitelist of specific token IDs
  • NFTXRangeEligibility - Range of token IDs (min to max)

Periphery Contracts (Non-Audited)

The periphery contracts enable single-transaction mint+wrap and unwrap+redeem operations. These contracts are not audited but are minimal in scope:

  • NFTXVaultWrapper: 19 lines - Simple ERC20Wrapper using OpenZeppelin v5.5.0
  • NFTXVaultRouter: 156 lines - Stateless router for batched operations

NFTXVaultWrapper (19 lines)

A simple ERC20 wrapper for NFTX vault tokens. One wrapper is deployed per vault. Wraps vTokens at 1:1 ratio.

Full Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Wrapper} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol";

/**
 * @title NFTXVaultWrapper
 * @notice A simple ERC20 wrapper for NFTX vault tokens (vTokens).
 * @dev Wraps vault tokens at 1:1 ratio. Deploy one wrapper per vault.
 *      Example: THC vault (vToken) -> wTHC wrapper (wrapped vToken)
 */
contract NFTXVaultWrapper is ERC20Wrapper {
    constructor(
        IERC20 underlyingVault,
        string memory name_,
        string memory symbol_
    ) ERC20(name_, symbol_) ERC20Wrapper(underlyingVault) {}
}

NFTXVaultRouter (156 lines)

Universal stateless router that enables single-transaction mint+wrap and unwrap+redeem operations. A single deployment serves all vaults.

Full Source Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {NFTXVaultWrapper} from "./NFTXVaultWrapper.sol";

/**
 * @title INFTXVaultMinimal
 * @notice Minimal interface for NFTX vault operations
 */
interface INFTXVaultMinimal {
    function assetAddress() external view returns (address);
    function mint(uint256[] calldata tokenIds, uint256[] calldata amounts) external returns (uint256);
    function redeem(uint256 amount, uint256[] calldata specificIds) external returns (uint256[] memory);
    function targetRedeemFee() external view returns (uint256);
}

/**
 * @title NFTXVaultRouter
 * @notice Universal router for NFTX vault operations with wrapping support.
 * @dev Stateless contract - single deployment serves all vaults.
 *      Enables single-transaction mint+wrap and unwrap+redeem operations.
 */
contract NFTXVaultRouter is IERC721Receiver {
    using SafeERC20 for IERC20;

    uint256 private constant BASE = 1e18;

    event MintAndWrap(
        address indexed vault,
        address indexed wrapper,
        address indexed user,
        uint256[] tokenIds,
        uint256 wrappedAmount
    );

    event UnwrapAndRedeem(
        address indexed vault,
        address indexed wrapper,
        address indexed user,
        uint256 amount,
        uint256[] redeemedIds
    );

    /**
     * @notice Mint vTokens from NFTs and wrap them in a single transaction.
     * @param vault The NFTX vault address
     * @param wrapper The wrapper contract for this vault
     * @param nft The NFT collection address
     * @param tokenIds The NFT token IDs to deposit
     * @return wrappedAmount The amount of wrapped tokens received
     */
    function mintAndWrap(
        address vault,
        address wrapper,
        address nft,
        uint256[] calldata tokenIds
    ) external returns (uint256 wrappedAmount) {
        require(tokenIds.length > 0, "No token IDs");
        require(INFTXVaultMinimal(vault).assetAddress() == nft, "NFT mismatch");
        require(address(NFTXVaultWrapper(wrapper).underlying()) == vault, "Wrapper mismatch");

        // Transfer NFTs from user to this contract
        IERC721 nftContract = IERC721(nft);
        for (uint256 i = 0; i < tokenIds.length; i++) {
            nftContract.safeTransferFrom(msg.sender, address(this), tokenIds[i]);
        }

        // Approve vault to take NFTs
        if (!nftContract.isApprovedForAll(address(this), vault)) {
            nftContract.setApprovalForAll(vault, true);
        }

        // Build amounts array (all 1s for ERC721)
        uint256[] memory amounts = new uint256[](tokenIds.length);
        for (uint256 i = 0; i < tokenIds.length; i++) {
            amounts[i] = 1;
        }

        // Mint vTokens to this contract
        INFTXVaultMinimal(vault).mint(tokenIds, amounts);

        // vTokens minted = tokenIds.length * 1e18 (0% mint fee)
        uint256 vTokenAmount = tokenIds.length * BASE;

        // Approve wrapper to take vTokens
        IERC20(vault).approve(wrapper, vTokenAmount);

        // Wrap vTokens and send to user
        NFTXVaultWrapper(wrapper).depositFor(msg.sender, vTokenAmount);

        wrappedAmount = vTokenAmount;
        emit MintAndWrap(vault, wrapper, msg.sender, tokenIds, wrappedAmount);
    }

    /**
     * @notice Unwrap tokens and redeem NFTs in a single transaction.
     * @param vault The NFTX vault address
     * @param wrapper The wrapper contract for this vault
     * @param amount The number of NFTs to redeem (in whole units)
     * @param specificIds Specific token IDs to redeem (empty for random)
     * @return redeemedIds The NFT token IDs redeemed
     */
    function unwrapAndRedeem(
        address vault,
        address wrapper,
        uint256 amount,
        uint256[] calldata specificIds
    ) external returns (uint256[] memory redeemedIds) {
        require(amount > 0, "Amount must be > 0");
        require(address(NFTXVaultWrapper(wrapper).underlying()) == vault, "Wrapper mismatch");

        // Calculate total wrapped tokens needed (base amount + fee for targeted)
        uint256 fee = 0;
        if (specificIds.length > 0) {
            fee = INFTXVaultMinimal(vault).targetRedeemFee() * specificIds.length;
        }
        uint256 totalWrapped = (amount * BASE) + fee;

        // Transfer wrapped tokens from user
        IERC20(wrapper).safeTransferFrom(msg.sender, address(this), totalWrapped);

        // Unwrap to get vTokens
        NFTXVaultWrapper(wrapper).withdrawTo(address(this), totalWrapped);

        // Approve vault to burn vTokens
        IERC20(vault).approve(vault, totalWrapped);

        // Redeem NFTs
        redeemedIds = INFTXVaultMinimal(vault).redeem(amount, specificIds);

        // Transfer NFTs to user
        address nft = INFTXVaultMinimal(vault).assetAddress();
        IERC721 nftContract = IERC721(nft);
        for (uint256 i = 0; i < redeemedIds.length; i++) {
            nftContract.safeTransferFrom(address(this), msg.sender, redeemedIds[i]);
        }

        emit UnwrapAndRedeem(vault, wrapper, msg.sender, amount, redeemedIds);
    }

    /**
     * @notice Required for receiving ERC721 tokens via safeTransferFrom
     */
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }
}

Events

VaultCreated

event VaultCreated(
    uint256 indexed vaultId,
    address vaultAddress,
    address assetAddress,
    string name,
    string symbol
);

Minted

event Minted(
    uint256[] tokenIds,
    uint256[] amounts,
    address indexed to
);

Redeemed

event Redeemed(
    uint256[] tokenIds,
    uint256[] amounts,
    address indexed to
);

Security

Audits

The core contracts are forked from NFTX V2, which has been audited by:

  • Code4rena
  • SECBIT
  • Trail of Bits

Periphery Contracts (Non-Audited)

The periphery contracts (NFTXVaultWrapper and NFTXVaultRouter) are not audited. They are:

  • Minimal: 175 total lines of Solidity
  • Stateless: Router holds no funds between transactions
  • Based on audited code: Uses OpenZeppelin v5.5.0 ERC20Wrapper

Users interact with wrapped tokens through the router. The full source code is included above for transparency.

Immutability

Once deployed, vault logic cannot be upgraded. This ensures:

  • Predictable behavior
  • No admin key risks
  • Trustless operation