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
THCnotvTHC - Singular form - Use
PUNKnotPUNKS
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):
| Contract | Address |
|---|---|
| NFTXVaultFactoryUpgradeable | 0x9bFF7c7a0E9799d0494F02551077069FBEa2Ff6F |
| NFTXVaultUpgradeable (impl) | 0x5c870C830D23dF7017d9ecE0Fa4b6119F489219B |
| NFTXSimpleFeeDistributor | 0x732A312B15250F7B43321f6e8A23212840ce4568 |
Periphery Contracts (Non-Audited):
| Contract | Address |
|---|---|
| NFTXVaultRouter | 0xa1d5F4c37a71D9021913dB1b57102CdE642797e5 |
| NFTXVaultWrapper | Deployed per vault via UI |
Mock NFT Collections (Testnet Only):
| Collection | Address | Max Supply |
|---|---|---|
| Tiny Hyper Cats | 0x2bf1e6d6776cA15558B71eB5fa8cA37CC9Dd513f | 2222 |
| Hypurr | 0x13e992622141bFa177ce42f5C9252DEa08891b55 | 4600 |
| Hypio | 0x87Db4A9f872Fe496166ea1b518e2c6c3D264cff7 | 5555 |
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 IDsNFTXRangeEligibility- 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