Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/hyperliquid-core-transfers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Hyperliquid Core transfers

This note scopes how Nexus can add Hyperliquid Core deposit and withdrawal flows on top of the existing HyperEVM warp routes.

## Boundary model

HyperEVM is already available as a Hyperlane chain, so the open work is the HyperCore <> HyperEVM boundary:

- Hyperliquid read precompiles are not a transfer path. They expose Core state to HyperEVM contracts.
- CoreWriter is the HyperEVM -> HyperCore write path. A contract calling CoreWriter can enqueue Core actions for that contract's own Core account.
- HyperCore -> HyperEVM does not call arbitrary EVM calldata. It credits the linked EVM asset through the protocol transfer path.
- HyperEVM -> HyperCore for generic linked spot assets is token-specific. For USDC, use Circle's `CoreDepositWallet.depositFor` so the adapter can credit the user's Core account.

## Supported product shapes

### Deposit into Hyperliquid Core

The clean first target is USDC:

1. User bridges USDC to HyperEVM through the existing warp route.
2. The HyperEVM recipient is either the user for a manual second leg, or a future adapter contract for a one-click flow.
3. The second leg approves Circle's CoreDepositWallet and calls `depositFor(user, amount, destinationDex)`.

Generic linked spot assets need more care. Sending from an adapter to a token system address may credit the adapter's Core account, not the user's Core account, unless the asset exposes a recipient-aware deposit path similar to USDC.

### Withdraw out of Hyperliquid Core

There is no direct HyperCore -> warp-route call path.

The practical staged flow is:

1. User signs a Hyperliquid Core action that brings the linked asset to the same address on HyperEVM.
2. Once the EVM balance is credited, Nexus uses the existing HyperEVM warp route as the second leg.

A future adapter can support contract-owned liquidity by recording an intent, calling CoreWriter for the adapter's Core account, waiting for the HyperEVM credit, then calling the warp route. It cannot pull arbitrary user Core balances.

## UI implications

- Treat Hyperliquid Core transfers as staged flows, not ordinary one-call warp routes.
- Keep the destination DEX explicit: `0` for perps and `uint32.max` for spot.
- Do not imply atomicity across Core <> EVM.
- Only advertise one-click adapter support for assets with recipient-aware Core deposit semantics.
81 changes: 81 additions & 0 deletions src/features/hyperliquid/coreTransfers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest';

import {
HYPERLIQUID_CORE_DEX,
HYPERLIQUID_CORE_WRITER,
HYPERLIQUID_HYPE_SYSTEM_ADDRESS,
HYPERLIQUID_USDC_SYSTEM_ADDRESS,
HyperliquidCoreCompletionMode,
HyperliquidCoreTransferKind,
getHyperliquidCoreTransferPlan,
} from './coreTransfers';

describe('Hyperliquid Core transfer planning', () => {
it('exposes stable Core constants', () => {
expect(HYPERLIQUID_CORE_WRITER).toBe('0x3333333333333333333333333333333333333333');
expect(HYPERLIQUID_HYPE_SYSTEM_ADDRESS).toBe('0x2222222222222222222222222222222222222222');
expect(HYPERLIQUID_USDC_SYSTEM_ADDRESS).toBe('0x2000000000000000000000000000000000000000');
expect(HYPERLIQUID_CORE_DEX.perps).toBe(0);
expect(HYPERLIQUID_CORE_DEX.spot).toBe(0xffffffff);
});

it('plans USDC deposits as recipient-aware CoreDepositWallet deposits', () => {
const plan = getHyperliquidCoreTransferPlan({
originChainName: 'base',
destinationChainName: 'hyperevm',
tokenSymbol: 'USDC',
});

expect(plan).toEqual({
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.UsdcDepositFor,
adapterCanCreditUser: true,
requiresUserSecondLeg: false,
});
});

it('does not claim adapter support for HYPE system transfers', () => {
const plan = getHyperliquidCoreTransferPlan({
originChainName: 'ethereum',
destinationChainName: 'hyperevm',
tokenSymbol: 'HYPE',
});

expect(plan).toEqual({
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.UserSystemTransfer,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
});
});

it('marks generic linked spot deposits as adapter-intent work', () => {
const plan = getHyperliquidCoreTransferPlan({
originChainName: 'solanamainnet',
destinationChainName: 'hyperevm',
tokenSymbol: 'SOL',
});

expect(plan).toEqual({
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.RequiresIntentAdapter,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
});
});

it('marks HyperEVM-origin routes as withdrawal second legs', () => {
const plan = getHyperliquidCoreTransferPlan({
originChainName: 'hyperevm',
destinationChainName: 'base',
tokenSymbol: 'USDC',
});

expect(plan).toEqual({
kind: HyperliquidCoreTransferKind.WithdrawFromCoreSecondLeg,
completionMode: HyperliquidCoreCompletionMode.None,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
});
});
});
90 changes: 90 additions & 0 deletions src/features/hyperliquid/coreTransfers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
export const HYPERLIQUID_EVM_CHAIN = 'hyperevm';

export const HYPERLIQUID_CORE_WRITER = '0x3333333333333333333333333333333333333333';
export const HYPERLIQUID_HYPE_SYSTEM_ADDRESS = '0x2222222222222222222222222222222222222222';
export const HYPERLIQUID_USDC_SYSTEM_ADDRESS = '0x2000000000000000000000000000000000000000';

export const HYPERLIQUID_CORE_DEX = {
perps: 0,
spot: 0xffffffff,
} as const;

export enum HyperliquidCoreTransferKind {
NotHyperliquid = 'not-hyperliquid',
DepositIntoCore = 'deposit-into-core',
WithdrawFromCoreSecondLeg = 'withdraw-from-core-second-leg',
}

export enum HyperliquidCoreCompletionMode {
None = 'none',
UsdcDepositFor = 'usdc-deposit-for',
UserSystemTransfer = 'user-system-transfer',
RequiresIntentAdapter = 'requires-intent-adapter',
}

export interface HyperliquidCoreTransferPlanInput {
originChainName: string;
destinationChainName: string;
tokenSymbol: string;
}

export interface HyperliquidCoreTransferPlan {
kind: HyperliquidCoreTransferKind;
completionMode: HyperliquidCoreCompletionMode;
adapterCanCreditUser: boolean;
requiresUserSecondLeg: boolean;
}

const USDC_SYMBOLS = new Set(['USDC', 'USDC.E']);
const HYPE_SYMBOLS = new Set(['HYPE', 'WHYPE']);

export function getHyperliquidCoreTransferPlan({
originChainName,
destinationChainName,
tokenSymbol,
}: HyperliquidCoreTransferPlanInput): HyperliquidCoreTransferPlan {
const normalizedSymbol = tokenSymbol.toUpperCase();

if (destinationChainName === HYPERLIQUID_EVM_CHAIN) {
if (USDC_SYMBOLS.has(normalizedSymbol)) {
return {
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.UsdcDepositFor,
adapterCanCreditUser: true,
requiresUserSecondLeg: false,
};
}

if (HYPE_SYMBOLS.has(normalizedSymbol)) {
return {
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.UserSystemTransfer,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
};
}

return {
kind: HyperliquidCoreTransferKind.DepositIntoCore,
completionMode: HyperliquidCoreCompletionMode.RequiresIntentAdapter,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
};
}

if (originChainName === HYPERLIQUID_EVM_CHAIN) {
return {
kind: HyperliquidCoreTransferKind.WithdrawFromCoreSecondLeg,
completionMode: HyperliquidCoreCompletionMode.None,
adapterCanCreditUser: false,
requiresUserSecondLeg: true,
};
}

return {
kind: HyperliquidCoreTransferKind.NotHyperliquid,
completionMode: HyperliquidCoreCompletionMode.None,
adapterCanCreditUser: false,
requiresUserSecondLeg: false,
};
}
Loading