HyperliquidVaultRouter Contract

Introduction

The HyperliquidVaultRouter acts as the bridge between user USDC deposits and the Devine OS vault system on Arbitrum. It exposes:

  • Deposit flow with entry fee, pending state, and admin confirmation to mint ERC4626 shares ("e.g DG**").

  • Withdrawal flow with quote locking, off-chain settlement, PnL computation, performance fee, and finalization via admin.

  • Batch liquidity management ensuring funds availability for withdrawals.

  • Role-based access control for sensitive operations.

Constants & Parameters

Name
Value / Description

_ENTRY_FEE_BASIS_POINTS

10 (i.e., 0.1% entry fee)

SETTLEMENT_LOCK_DURATION

5 minutes (window during which a withdrawal quote is valid)

Performance fee

30% of positive PnL on withdrawal

Immutables

Variable
Description

underlyingAsset

USDC ERC20 token (must have 6 decimals)

treasuryWallet

Address receiving entry and performance fees

vaultWallet

Working/trading wallet receiving deposit capital

Overview

Users deposit USDC to receive vault shares (“DG**”) after admin confirmation. Withdrawals are initiated by users (locking a quote), settled off-chain (preparing batch liquidity), and finalized by an admin who calculates PnL, applies performance fees, and transfers net USDC back to the user.

Key flows:

  1. Deposit: User calls deposit; funds are split (fee + trading capital), a PendingDeposit is recorded, and shares are minted only after admin calls confirmDeposit.

  2. Withdrawal: User calls initiateWithdrawal to snapshot state; admin prepares batch liquidity and calls confirmWithdrawalWithPnL to finalize, applying PnL logic and fees.

  3. Batch Settlement: Batches are tracked by quoteTimestamp + batchNonce hashed into a batchKey; liquidity must be confirmed before withdrawal consumption.

Structs

PendingDeposit

  • amount: Post-fee amount to credit.

  • timestamp: Initiation time.

  • nonce: User-specific deposit nonce.

  • confirmed: Whether finalized.

  • navTimestamp: Snapshot of NAV freshness.

PendingWithdrawal

  • shares: Shares to redeem.

  • timestamp: Initiation time.

  • nonce: User-specific withdrawal nonce.

  • confirmed: Whether finalized.

WithdrawalQuote

  • shares: Shares being withdrawn.

  • navAtQuote: Vault NAV snapshot (totalAssets()).

  • supplyAtQuote: Total share supply at quote.

  • userDepositAtQuote: User’s confirmed deposit snapshot.

  • userSharesAtQuote: User’s total shares snapshot.

  • timestamp: Quote creation time.

Functions

deposit

function deposit(uint256 assets, address receiver) external override nonReentrant returns (uint256);

Initiates a deposit:

  • Requires assets > 0 and within maxDeposit.

  • Deducts 0.1% entry fee (sent to treasuryWallet); remainder forwarded to vaultWallet.

  • Records PendingDeposit with nonce and NAV snapshot.

  • Does not mint shares; returns previewDeposit(depositAfterFee) (expected shares after confirmation).

  • Emits: DepositInitiated, VaultOutflowToPlatform, FeeTransferred.

confirmDeposit

function confirmDeposit(address user, uint256 amount, bytes32 txHash, uint256 nonce) external nonReentrant onlyRole(ADMIN_ROLE);

Admin finalizes a pending deposit:

  • Validates uniqueness of txHash (both per-user and global).

  • Checks stored pending deposit matches amount and is unconfirmed.

  • Enforces NAV freshness: lastExchangeBalanceTimestamp >= pd.timestamp.

  • Mints shares via previewDeposit(amount); updates userTotalShares and aggregate counters.

  • Emits: DepositConfirmed.

initiateWithdrawal

function initiateWithdrawal(uint256 shares) external nonReentrant returns (uint256 nonce);

User locks in a withdrawal quote:

  • Requires sufficient share balance.

  • Snapshots current state in WithdrawalQuote.

  • Records a PendingWithdrawal.

  • Returns a user-specific nonce.

  • Emits: WithdrawalInitiated.

confirmWithdrawalWithPnL

function confirmWithdrawalWithPnL(
    address user,
    uint256 shares,
    bytes32 txHash,
    uint256 nonce,
    int256 userPnl,
    uint256 batchNonce,
    uint256 navAtSettlement
) external nonReentrant onlyRole(ADMIN_ROLE);

Admin finalizes a withdrawal:

  1. Derives batchKey = keccak256(quote.timestamp, batchNonce).

  2. Validates idempotency (txHash, batch not already used).

  3. Ensures pending withdrawal matches expectation.

  4. Requires batch liquidity exists: batchUsdcRemaining[batchKey] > 0.

  5. Computes:

    • withdrawalAmount = floor(shares * navAtSettlement / quote.supplyAtQuote)

    • userDepositReduction = floor(shares * quote.userDepositAtQuote / quote.userSharesAtQuote)

    • calculatedPnl = int(withdrawalAmount) - int(userDepositReduction)

    • Performance fee: 30% of positive calculatedPnl

    • Net PnL and withdrawal amount after fee.

  6. Verifies provided userPnl matches computed net PnL.

  7. Burns user shares, updates state, transfers:

    • Fee to treasuryWallet

    • Net USDC to user

  8. Updates userPnL, confirmedUserWithdrawals.

  9. Emits: UserPnLReported, WithdrawalConfirmed.

confirmBatchUsdcReceived

function confirmBatchUsdcReceived(uint256 quoteTimestamp, uint256 batchNonce, uint256 usdcReadyNet) external onlyRole(ADMIN_ROLE);

Registers available USDC for a batch:

  • Derives batchKey.

  • Sets batchUsdcReceivedAt and batchUsdcRemaining.

  • Emits: BatchUsdcReceivedConfirmed.

AdminconfirmBridgeReceipt

function AdminconfirmBridgeReceipt(
    uint256 quoteTimestamp,
    uint256 batchNonce,
    uint256 amount,
    bytes32 hyperliquidTxHash,
    string calldata hyperliquidAddress
) external onlyRole(ADMIN_ROLE);

Logs off-chain bridge receipt tied to a batch. Emits BridgeWithdrawalReceipt.

updateVaultExchangeBalance

function updateVaultExchangeBalance(uint256 newBalance) external onlyRole(ADMIN_ROLE) nonReentrant;

Updates the vault’s NAV (latestExchangeBalance) and timestamp. Emits ExchangeBalanceUpdated.

Utility / View Functions

function totalAssets() public view override returns (uint256);
function getVaultLiquidity() public view returns (uint256);
function getNextExpectedNonce(address user) public view returns (uint256);
function scaleAmount(uint256 amount, uint8 fromDecimals, uint8 toDecimals) public pure returns (uint256);
  • totalAssets returns the externally-updated NAV.

  • getVaultLiquidity returns USDC held in the router.

  • getNextExpectedNonce helps clients derive upcoming deposit nonce.

  • scaleAmount converts between decimals safely.

Admin Emergency / Force Utilities

  • adminForceConfirmDeposit(...): Force-confirms a pending deposit without NAV freshness enforcement.

  • adminCancelPendingDeposit(...): Cancels a pending deposit, reconstructs original gross amount, pulls fee back from treasury, and refunds user.

  • adminForceConfirmWithdrawal(...): Force-finalizes withdrawal with provided PnL and net amount.

  • adminCancelPendingWithdrawal(...): Marks pending withdrawal as confirmed (no fund movement).

  • adminForceConfirmBatchUsdc(...): Force-sets batch USDC availability (alias to confirmBatchUsdcReceived).

  • seedVault(uint256 assets): One-time initialization; seeds vault, forwards assets to treasury, mints equivalent shares, sets NAV.

Access Control Management

function grantAdminRole(address account) external onlyRole(DEFAULT_ADMIN_ROLE);
function revokeAdminRole(address account) external onlyRole(DEFAULT_ADMIN_ROLE);
function setPlatformWallet(address newWallet) external onlyRole(DEFAULT_ADMIN_ROLE);
  • Manages ADMIN_ROLE and updates vaultWallet.

Events

DepositInitiated

event DepositInitiated(address indexed user, uint256 amount, uint256 nonce);

VaultOutflowToPlatform

event VaultOutflowToPlatform(address indexed user, uint256 amount, address platformWallet);

FeeTransferred

event FeeTransferred(address indexed user, uint256 fee, address treasuryWallet);

DepositConfirmed

event DepositConfirmed(address indexed user, uint256 amount, uint256 shares, bytes32 txHash);

WithdrawalInitiated

event WithdrawalInitiated(address indexed user, uint256 shares, uint256 nonce);

WithdrawalConfirmed

event WithdrawalConfirmed(
    address indexed user,
    uint256 amount,
    uint256 shares,
    bytes32 txHash,
    uint256 userDepositReduction
);

ExchangeBalanceUpdated

event ExchangeBalanceUpdated(uint256 newBalance, uint256 timestamp);

UserPnLReported

event UserPnLReported(address indexed user, int256 userPnl);

PendingDepositCancelled

event PendingDepositCancelled(
    address indexed user,
    uint256 indexed nonce,
    uint256 amount,
    address indexed vaultWallet,
    address cancelledBy
);

BatchUsdcReceivedConfirmed

event BatchUsdcReceivedConfirmed(bytes32 indexed batchKey, uint256 usdcReadyNet);

BridgeWithdrawalReceipt

event BridgeWithdrawalReceipt(
    bytes32 indexed batchKey,
    uint256 amount,
    bytes32 hyperliquidTxHash,
    string hyperliquidAddress
);

Errors

  • InsufficientDepositAmount(uint256 depositAmount, uint256 fee) — deposit after fee is zero or negative.

  • Standard ERC4626 errors (e.g., ERC4626ExceededMaxDeposit) for limits.

Calculations

Entry Fee

fee = assets * 0.1%
depositAfterFee = assets - fee

Share Minting (on confirmed deposit)

Uses previewDeposit(depositAfterFee) which applies ERC4626 conversion based on current vault state.

Withdrawal Amount

withdrawalAmount = floor(shares * navAtSettlement / quote.supplyAtQuote)

User Deposit Reduction

userDepositReduction = floor(shares * quote.userDepositAtQuote / quote.userSharesAtQuote)

PnL

calculatedPnl = int(withdrawalAmount) - int(userDepositReduction)

Performance Fee

If calculatedPnl > 0:

performanceFee = floor(positivePnl * 30%)
netPnl = calculatedPnl - performanceFee
netWithdrawalAmount = withdrawalAmount - performanceFee

Else:

netPnl = calculatedPnl (negative)
netWithdrawalAmount = withdrawalAmount

Batch Key Derivation

batchKey = keccak256(abi.encodePacked(quote.timestamp, batchNonce))

Used to identify and track batch liquidity readiness and consumption.

Notes & Observations

  • The settlement window is 5 minutes; confirmation of withdrawal must occur before expiry.

  • NAV freshness is enforced on deposit confirmation: the lastExchangeBalanceTimestamp must be >= deposit initiation timestamp.

  • adminCancelPendingDeposit requires treasury to have approved fee transfer back — operationally ensure proper allowances or adjust flow.

  • There is a potential logic inversion in quote validation inside confirmWithdrawalWithPnL comparing snapshots (audit recommended).

  • Role differentiation between ADMIN_ROLE and DEFAULT_ADMIN_ROLE should be clearly documented or consolidated.

Example Sequence (High-Level)

  1. User approves USDC and calls deposit.

  2. Admin updates NAV if needed via updateVaultExchangeBalance.

  3. Admin calls confirmDeposit to mint shares.

  4. User calls initiateWithdrawal to get a quote.

  5. Admin calls confirmBatchUsdcReceived to mark liquidity ready.

  6. Admin calls confirmWithdrawalWithPnL to finalize and transfer USDC.


Last updated