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
_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
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:
Deposit: User calls
deposit
; funds are split (fee + trading capital), aPendingDeposit
is recorded, and shares are minted only after admin callsconfirmDeposit
.Withdrawal: User calls
initiateWithdrawal
to snapshot state; admin prepares batch liquidity and callsconfirmWithdrawalWithPnL
to finalize, applying PnL logic and fees.Batch Settlement: Batches are tracked by
quoteTimestamp
+batchNonce
hashed into abatchKey
; liquidity must be confirmed before withdrawal consumption.
Structs
PendingDeposit
PendingDeposit
amount
: Post-fee amount to credit.timestamp
: Initiation time.nonce
: User-specific deposit nonce.confirmed
: Whether finalized.navTimestamp
: Snapshot of NAV freshness.
PendingWithdrawal
PendingWithdrawal
shares
: Shares to redeem.timestamp
: Initiation time.nonce
: User-specific withdrawal nonce.confirmed
: Whether finalized.
WithdrawalQuote
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 withinmaxDeposit
.Deducts 0.1% entry fee (sent to
treasuryWallet
); remainder forwarded tovaultWallet
.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)
; updatesuserTotalShares
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:
Derives
batchKey = keccak256(quote.timestamp, batchNonce)
.Validates idempotency (
txHash
, batch not already used).Ensures pending withdrawal matches expectation.
Requires batch liquidity exists:
batchUsdcRemaining[batchKey] > 0
.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.
Verifies provided
userPnl
matches computed net PnL.Burns user shares, updates state, transfers:
Fee to
treasuryWallet
Net USDC to user
Updates
userPnL
,confirmedUserWithdrawals
.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
andbatchUsdcRemaining
.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 toconfirmBatchUsdcReceived
).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 updatesvaultWallet
.
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
andDEFAULT_ADMIN_ROLE
should be clearly documented or consolidated.
Example Sequence (High-Level)
User approves USDC and calls
deposit
.Admin updates NAV if needed via
updateVaultExchangeBalance
.Admin calls
confirmDeposit
to mint shares.User calls
initiateWithdrawal
to get a quote.Admin calls
confirmBatchUsdcReceived
to mark liquidity ready.Admin calls
confirmWithdrawalWithPnL
to finalize and transfer USDC.
Last updated