HyperVaultsFund platform · Hyperliquid
HyperEVM Testnet

HyperVaults Protocol

v0.2 · Solidity 0.8.26 · HyperEVM Testnet (chain 998) · 416 tests · Unaudited

HyperVaults is an institutional on-chain asset management protocol built natively on Hyperliquid. Any trader can launch a professionally structured vault with institutional-grade fee structures, on-chain risk enforcement, and real-time NAV via HyperCore precompiles. Every deposit settles directly on HyperEVM, every position settles on Hyperliquid's order book — no bridges, no relayers.

Overview

27 Solidity files, ~6.8k lines of code, deployed on HyperEVM. The architecture deliberately splits storage, business logic, and extension modules into distinct contracts, enabling modular policy enforcement and clean upgrade paths via a release registry. 407 non-fork tests + 9 fork-based lifecycle tests pass.

Vault shares are 18-decimal ERC-20 tokens (denomination-asset agnostic via OpenZeppelin's virtual-offset pattern, _decimalsOffset = 18 − underlyingDecimals). Inflation attacks are mitigated by virtual shares + virtual assets:+ 10^12 virtual shares for a USDC vault means an attacker would need to donate ~10^12× a victim's deposit to extract a single wei.

Quick Start

Deploy a Vault

VaultFactory.VaultConfig memory config = VaultFactory.VaultConfig({
    name:                    "Alpha Trading Strategy",
    symbol:                  "ALPHA",
    managementFeeBps:        100,    // 1.00% / yr
    managementFeeRecipient:  manager,
    performanceFeeBps:       2000,   // 20.00% above HWM
    performanceFeeRecipient: manager,
    lockUpDuration:          1 days, // per-depositor rolling lock-up
    sharesNonTransferrable:  false   // immutable after deploy
});

(address vault, address controller) = factory.createVault(config);

Deposit

usdc.approve(controller, 1_000e6);

// minShares is slippage protection — 0 accepts any.
controller.buyShares(1_000e6, 0);

Open a Perpetual Position

Position actions go through the ExternalPositionManager. Args are HyperCore-native: uint32 asset id (HL universe index for canonical, encoded for HIP-3 / HIP-4), uint64 size and price at 1e8 scaling.

bytes memory args = abi.encode(
    /* asset   */ uint32(0),       // BTC = canonical universe index 0
    /* isBuy   */ true,
    /* limitPx */ uint64(70_000 * 1e8),
    /* size    */ uint64(0.1 * 1e8),
    /* tif     */ uint8(2)         // 2 = GTC
);

externalPositionManager.callOnExternalPosition(
    vault, position, ACTION_OPEN_POSITION /* = 0 */, args
);

Redeem Shares

// Fast path if vault has idle USDC; otherwise queues a NAV-locked
// claim that settles when idle USDC is replenished.
controller.redeem(
    /* recipient */ msg.sender,
    /* shares    */ 500e18,
    /* minUsdcOut*/ 0
);

Architecture

VaultFactory  ── deploys + configures vaults (CREATE2 deterministic addresses)
    │
    ├── VaultLib        ── ERC-20 shares (18-dec via offset) + asset custody
    │       └── External Positions   ── Hyperliquid perps (canonical, HIP-3)
    │                                    HIP-4 outcome contracts
    │
    └── VaultController ── deposit / redeem / NAV / extension wiring
            ├── FeeManager               ── mgmt + performance fees as shares
            ├── PolicyManager            ── 5 hooks · per-vault settings
            ├── ValueInterpreter         ── HyperCore oracle pricing (0x803)
            ├── ExternalPositionManager  ── action gating · type registry
            └── RedemptionQueue          ── NAV-locked claims when idle low

ReleaseRegistry versions the implementation set (lib + controller + extensions). New vaults pin to latestReleaseId at deploy; existing vaults are unaffected by future releases.

Contract Inventory

ContractLinesPurpose
VaultLib.sol538ERC-20 share token, asset custody, position registry
VaultController.sol753Deposit / redeem / NAV / extension wiring
VaultFactory.sol347Deterministic vault deployment + create-with-policies
ValueInterpreter.sol260Asset pricing via HyperCore oracle precompile
ReleaseRegistry.sol181Versioned implementation set, latest pin at deploy
FeeManager.sol461Mgmt + performance fees, accrued and paid as shares
PolicyManager.sol3315-hook policy engine, per-vault settings registry
ExternalPositionManager.sol470Position lifecycle + action gating + parser dispatch
RedemptionQueue.sol329FIFO NAV-locked USDC claims when idle is low
HyperliquidPositionLib.sol715Perp + spot accounting via HyperCore precompiles
HyperliquidPositionParser.sol237Action arg parsing + canonical policy tuple
OutcomePositionLib.sol546HIP-4 outcome contract accounting
OutcomeParser.sol228HIP-4 action parsing + asset-id encoding
VaultLens.sol361Read-side aggregator for FE — summaries, positions, claims

VaultLib

Storage + ERC-20 share token + custody. Inherits ERC20Upgradeable and overrides decimals() to return underlyingDecimals + offset, standardising shares at 18 decimals across any underlying ≤ 18.convertToShares / convertToAssets use OZ's Math.mulDiv with virtual offsets, both rounding down.

Holds the registry of activeExternalPositions and gates every state mutation behind onlyVaultController. Two-step ownership transfer (offer + accept) prevents accidental ownership loss.

VaultController

Business logic for one vault. Holds the deposit / redeem / position-action surface and coordinates with extensions.

FunctionEffect
buyShares(amount, minShares)Pulls USDC, runs PreBuyShares policy hook, mints shares via convertToShares, settles pending fees, runs PostBuyShares.
redeem(recipient, shares, minUsdcOut)Burns shares, computes USDC owed via convertToAssets. Fast-path if vault holds enough idle USDC; otherwise pays partial and enqueues a NAV-locked claim in the RedemptionQueue.
callOnExternalPosition(pos, actionId, args)Owner-only. Routes through ExternalPositionManager which runs PreCallOnExternalPosition policy hook with the parser's canonical (uint32 asset, bool isLong, uint64 size, uint64 price) tuple before forwarding to the position lib.
calcGrossShareValue()Returns convertToAssets(10^shareDecimals) in underlying decimals (1e6 for USDC). Used by VaultLens.sharePrice.

All share-mutating actions are guarded by timeLockSharesAction (per-account 60s timelock), preventing same-block deposit-then-redeem griefing.

VaultFactory

Deterministic vault deployment via CREATE2. Reads the denomination asset's decimals at construction (validates ≤ 18) and forwards them through to VaultLib.init so the offset is baked in.

function createVault(VaultConfig calldata config)
    external returns (address vault, address controller);

function createVaultWithPolicies(
    VaultConfig calldata config,
    address[] calldata policies,
    bytes[]   calldata settings
) external returns (address vault, address controller);

lockUpDuration defaults to 1 day if 0, capped at 30 days. sharesNonTransferrable is immutable after deploy — set it at create time, not via a follow-up tx.

FeeManager

Two fees, both accrued and paid in vault shares (no USDC movements; depositors are diluted instead):

FeeFormulaCap
Managementshares × bps / 10000 annualised, accrued per-second.10.00% / yr
Performanceprofit × bps / 10000 where profit = GAV − HWM × supply / oneShare.50.00%

HWM is denominated in "underlying per 1.0 share" and only ratchets up. Performance fee is skipped when the current share price is below HWM, preventing double-charging during recovery.

PolicyManager

Hook-driven policy enforcement. Each enabled policy registers against one or more hooks; the manager iterates the active list and short-circuits on the first failure.

HookWhen fired
PreBuySharesBefore share mint in buyShares
PostBuySharesAfter mint — used for cumulative checks (per-user caps).
PreCallOnExternalPositionBefore any position action; receives the parser's canonical tuple.
PreTransferSharesBefore share transfer — enforces non-transferrable and allowlist on transfer recipient.
ContinuousKeeper-callable validation for time-window invariants.

ValueInterpreter

Asset → USD pricing. Reads HyperCore oracle prices via the 0x...0803 precompile, with a manual-price fallback for assets the oracle doesn't cover (used today for testnet USDC = $1.00). The denomination-asset registry gates which assets a vault can be denominated in.

ExternalPositionManager

Lifecycle and dispatch for non-vault asset positions. Position types are registered with a (lib, parser, label) triple. The current registry:

typeIdLabelLib
1Hyperliquid Perps (canonical + HIP-3)HyperliquidPositionLib
2HIP-4 Outcome ContractsOutcomePositionLib

callOnExternalPosition first callsparseAssetsForAction on the parser to learn transfer flows, withdraws assets from the vault to the position, runs the PreCallOnExternalPosition policy hook with the canonical tuple from encodePolicyArgs, then executes the action.

RedemptionQueue

FIFO queue of NAV-locked USDC claims. When redeem can't fully settle from idle USDC, the residual is enqueued at the share price at the moment of burn — exiters get NAV protection, but stayers absorb the P&L on the locked-up assets until claims clear.

Settles automatically when the manager closes positions or removes margin. forceCloseForQueue lets keepers unwind the largest perp position when claims have aged past a threshold.

HyperCore Precompiles

The protocol reads HyperCore state via stateless EVM precompiles. All reads are staticcall + abi-decode; no cross-domain trust assumptions.

AddressFunctionUsed by
0x...0803Oracle PriceValueInterpreter
0x...0813Position2 (HIP-3 aware)HyperliquidPositionLib
via PrecompileLibspotBalance / spotPx / markPx / withdrawable / spotInfoPosition libs (spot NAV, sub-account margin, mark for perps).

position2 is used uniformly so the canonical and HIP-3 paths share code, future-proofing against any canonical asset id eventually crossing 2^16.

CoreWriter

Writes go through @hyper-evm-lib/CoreWriterLib. The position lib calls placeLimitOrder, cancelOrderByCloid, usdClassTransfer (for USDC perp ↔ spot moves), and sendAsset (cross-DEX with non-USDC) — no manual action encoding. Asset routing is encoded into the asset id itself; no separate dex parameter on the write side.

AllowedAssetsPolicy

Restricts opens to a manager-curated uint32[] of asset ids. Encoding is HL-canonical: ids 0..9_999 are canonical perps, 100_000 × dexIdx + marketIdx is HIP-3, and 100_000_000 + 10·outcomeId + side is HIP-4. Ranges don't overlap, so a single uint32[] covers all three classes.

Settings: abi.encode(uint32[]). Hook: PreCallOnExternalPosition.

AllowlistPolicy

Address gate for deposits and share transfers. Vault owner is always implicitly included.

Settings: abi.encode(address[]). Hooks: PreBuyShares, PreTransferShares.

MaxLeveragePolicy

Caps the vault's leverage ratio (notional / GAV). Stored as 1e18-scaled multiplier, e.g. 5e18 = 5×.

Settings: abi.encode(uint256). Hook: PreCallOnExternalPosition.

MaxPositionSizePolicy

Limits any single position's notional as a % of GAV. Setting is bps-scaled (4000 = 40% of GAV).

Settings: abi.encode(uint256). Hook: PreCallOnExternalPosition.

MinMaxInvestmentPolicy

Per-deposit floor and ceiling, denominated in underlying. max = 0 means no ceiling.

Settings: abi.encode(uint256 min, uint256 max). Hook: PreBuyShares.

MaxTotalAumPolicy

Hard cap on total deposits across the vault. Reverts buys when GAV + amount > cap.

Settings: abi.encode(uint256). Hook: PreBuyShares.

MaxUserDepositPolicy

Cap on a single LP's position size, evaluated after the deposit so the policy bites cumulatively.

Settings: abi.encode(uint256). Hook: PostBuyShares.

PerpPriceDivergencePolicy

Blocks opens when the limit price diverges from the HyperCore mark by more than the configured tolerance (in bps × 100). Guards against fat-finger and oracle-staleness opens.

Settings: abi.encode(uint16). Hook: PreCallOnExternalPosition.

Guide: Create a Vault

Use VaultFactory.createVaultWithPolicies to ship the vault and its policies in one tx — atomic, cheaper, and avoids a window where the vault accepts deposits before policies are wired.

address[] memory policies = new address[](2);
bytes[]   memory settings = new bytes[](2);

policies[0] = MAX_LEVERAGE_POLICY;
settings[0] = abi.encode(uint256(5e18));   // 5× cap

policies[1] = ALLOWED_ASSETS_POLICY;
uint32[] memory allowed = new uint32[](2);
allowed[0] = 0;   // BTC canonical
allowed[1] = 1;   // ETH canonical
settings[1] = abi.encode(allowed);

(address vault, address controller) =
    factory.createVaultWithPolicies(config, policies, settings);

Guide: Deposit & Redeem

Deposit:

usdc.approve(controller, amount);
controller.buyShares(amount, 0);

Redeem (fast path, vault has idle USDC):

controller.redeem(msg.sender, shares, 0);

If the vault doesn't hold enough idle USDC, the call still succeeds but pays you partially and enqueues the residual as a NAV-locked claim. Watch for RedemptionPartiallySettled in the receipt and check VaultLens.getUserClaims for status.

Guide: Open a Position

All position actions are owner-only and routed through VaultController.callOnExternalPosition. Action args follow the parser's decode shape — for ACTION_OPEN_POSITION = 0:

bytes memory args = abi.encode(
    uint32 asset,    // HL asset id (canonical / HIP-3 / HIP-4)
    bool   isBuy,
    uint64 limitPx,  // 1e8-scaled
    uint64 size,     // 1e8-scaled
    uint8  tif       // 1=ALO, 2=GTC, 3=IOC
);

vaultController.callOnExternalPosition(perpPosition, 0, args);

Other action ids on HyperliquidPositionLib: 1 close, 2 cancel, 3 add margin (with uint32 dex selector), 4 remove margin, 10 open spot, 20 transfer USDC perp ↔ spot.

Guide: Collect Fees

Fees are minted as vault shares to the configured recipients during any state-changing action (deposit, redeem, position open / close). Recipients can:

// 1) Hold the shares — they appreciate with vault NAV.
// 2) Redeem to USDC, same surface as any LP.
controller.redeem(recipient, shareBalance, 0);

Use VaultLens.getVaultSummary to read pendingMgmtFeeShares and pendingPerfFeeShares — fees that have accrued since the last settlement and will mint on the next state change.

This software is unaudited and on testnet. Perpetual-futures trading carries significant risk. Never deploy real funds without an independent audit.