The Hyperliquid Runbook

Zeel PatelZeel Patel
27 min read

Table of contents

Why I wrote this
After shipping a full Core↔EVM link (auction → metadata → genesis → Step‑1/Step‑2 → transfers → optional Hyperliquidity) at my workplace, I realized there wasn't a single builder‑level doc that captured every pitfall we hit (proxies, CREATE2, decimals, vault funding, SDK payloads, Composer deets). This is the guide I wish I had on day one.

Who should use this
Hyperliquid‑native teams (No pre-existing ERC-20)
Existing‑token teams (already have an ERC‑20 or a HIP‑1)

Attribution & thanks
This runbook leans on official Hyperliquid docs and the LayerZero Hyperliquid Composer documentation. Where relevant, I quote and credit the LayerZero team and link to the original sections so you can verify details yourself. I also thank Filip and Daniel from my team in their contributions to navigating the complexities. Connect with me on X here.


Critical warnings (read before linking)

“Hyperliquid has no checks for asset bridge capacity. If you try to bridge more tokens than available on the destination side of the bridge, all tokens will be locked in the asset bridge address forever.”

“Partially funding the HyperCore asset bridge is problematic… Always fully fund the HyperCore side of the asset bridge with the total intended circulatable supply via the bridge.”

https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts

Transferring tokens from HyperEVM to HyperCore can be done using an ERC20 transfer with the corresponding system address as the destination… credited based on the emitted Transfer(from, to, value).

— Hyperliquid Transfers: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyperevm-transfers

My take: For non‑canonical links, treat the asset bridge (system) address as your payout treasury: pre‑fund it on the destination chain before inviting users to bridge. For canonical links, mint/burn but still canary first.


Runbook

Ordered exactly as you’ll work through it from Prep to Final deployment.

Prep

<PREP OVERVIEW STARTS - DO NOT FOLLOW STEPS YET, THIS OVERVIEW IN PREP IS TO GIVE YOU A VERY THOROUGH IDEA OF PROCESS BEFORE STARTING>

  • Accounts & funding: Activate deployer on HyperCore (≥ $1 USDC/HYPE) so L1 actions work. (If not, you’ll see User or API Wallet does not exist.)

  • Tooling: Node + pnpm/yarn, Foundry/Hardhat, @layerzerolabs/hyperliquid-composer installed. Keep a .env with keys.

  • Block size: Large contract deploys need Big blocks on HyperEVM.

npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PK
# (later) switch back\ nnpx @layerzerolabs/hyperliquid-composer set-block --size small --network mainnet --private-key $PK

UI vs SDK, which to use

  • HL‑native (no existing ERC‑20): UI is fine for auction/metadata/genesis and listing, still read bridge warnings first. Composer/SDK needed to deploy OFT + Composer if you want cross‑chain inflow.

  • Existing token: Prefer LayerZero Hyperliquid SDK (Composer) for deterministic steps, block switching, and safe linking. UI forces Hyperliquidity so avoid that route if you have a MM that will market-make your orderbook.

1) Buy a Core Spot index

2) Deploy HIP‑1 (Core)

Create the Core spot token, set user genesis (balances), confirm, register the spot, (optionally) register Hyperliquidity scaffolding.

(THIS IS PREP GUIDE TO RUN THROUGH THE PROCESS FIRST, DONT GO THROUGH WITH STEPS JUST YET)

  • Composer (recommended):
# 2.1 create deployment state
npx @layerzerolabs/hyperliquid-composer create-spot-deployment \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# 2.2 set user genesis (writes u64 balances; can mint to asset bridge or deployer)
npx @layerzerolabs/hyperliquid-composer user-genesis \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# 2.3 confirm (locks genesis)
npx @layerzerolabs/hyperliquid-composer set-genesis \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# 2.4 register spot (USDC‑quoted orderbook primitive)
npx @layerzerolabs/hyperliquid-composer register-spot \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# 2.5 register hyperliquidity (set nOrders=0 if not launching HIP‑2 yet)
npx @layerzerolabs/hyperliquid-composer create-spot-deployment \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# 2.6 (optional) set trading fee share (commonly 100%)
npx @layerzerolabs/hyperliquid-composer trading-fee \
  --token-index $CORE_INDEX --share "100%" --network mainnet --private-key $CORE_PK
  • Notes) “HyperCore balances are u64… max 18446744073709551615.” “Only USDC is supported on HyperCore at the moment.” — LayerZero OFT Guide.

3) Deploy (or reuse) your ERC‑20 on HyperEVM (OFT, plain ERC‑20, or other bridges)

Hyperliquid linking works with any standard ERC‑20 on HyperEVM. You can deploy a fresh token, reuse an existing one (including tokens originated from other bridges like Wormhole), or use LayerZero’s OFT stack. What matters for Hyperliquid is:

  1. the ERC‑20 emits standard Transfer events, and

  2. you select the correct Step‑2 proof (CREATE vs CREATE2 vs proxy).

3.1 Pick your scenario

  • A) New plain ERC‑20

    • Deploy with your usual toolchain (Foundry/Hardhat). If contract is upgradeable, link the proxy (never the implementation). If created via factory/CREATE2, plan to use a storage‑slot finalize proof in Step‑2.

    • Interface sanity: transfer, transferFrom, balanceOf, decimals, symbol; must emit Transfer(address,address,uint256).

    • Token behaviors to avoid: fee‑on‑transfer, rebasing, blacklists, pausable hooks that could block the system/asset‑bridge address (bridge won’t be able to pay out).

  • B) OFT‑based ERC‑20 (LayerZero)

    • Bootstrapping repo (includes Composer + block switching helpers):

        LZ_ENABLE_EXPERIMENTAL_HYPERLIQUID_EXAMPLE=1 npx create-lz-oapp@latest
      
    • Follow the SDK to deploy your OFT and the HyperLiquidComposer. For heavy deploys, switch to Big blocks first (then back to small):

        npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PK
        # ...deploy...
        npx @layerzerolabs/hyperliquid-composer set-block --size small --network mainnet --private-key $PK
      
    • OFT path is convenient if you want the Composer flow for EVM→Core sends (approve + send → Composer moves to bridge → Core credit).

  • C) Existing ERC‑20 (non‑OFT, including tokens minted via other bridges, e.g., Wormhole)

    • You can reuse it as‑is. Hyperliquid requires only the ERC‑20 address and a valid finalize proof.

    • Checklist:

      1. Confirm decimals() and compute evmExtraWeiDecimals = EVM.decimals − CORE.weiDecimals.

      2. If upgradeable, identify the proxy and plan a custom storage‑slot proof (EIP‑1967 implementation slot) for Step‑2.

      3. Ensure the token doesn’t restrict transfers from the asset‑bridge (system) address; no blacklists/pauses.

      4. Plan supply topology: if you expect Core→EVM withdrawals soon and your link is non‑canonical, pre‑fund the EVM system address (vault) with enough tokens to cover withdrawals. If your circulating supply originates on Core, pre‑fund the Core asset‑bridge side via genesis.

    • Note on third‑party bridges: Linking to Hyperliquid is independent of other bridges, just avoid double creating circulating supply. Decide which side is canonical and fully fund the destination bridge address before users move.

3.2 Sanity checks (works for all scenarios)

  • Confirm decimals & symbol

      cast call $ERC20 "decimals()(uint8)"
      cast call $ERC20 "symbol()(string)"
    
  • Quick Transfer log test (dry‑run small amount)

      # send 1 unit (adjust for decimals) to your own addr to see a Transfer log
      cast send $ERC20 "transfer(address,uint256)" $YOUR_ADDR 1 --private-key $PK
    
  • Plan the decimal diff used in linking

      evmExtraWeiDecimals = EVM_DECIMALS - CORE_WEI_DECIMALS
    

3.3 Deployment tips

  • Big blocks only needed for large bytecode deployments, switching is safe to do via Composer CLI and reversible.

  • CREATE vs CREATE2 vs Proxy

    • EOA + CREATE → later use create { nonce } proof.

    • Factory/CREATE2 → plan firstStorageSlot or customStorageSlot proof.

    • Proxy (UUPS/Transparent) → link proxy; use customStorageSlot with EIP‑1967 implementation slot.

3.4 Using Composer with non‑OFT tokens (optional)

You don’t need OFT to bridge via Hyperliquid. Users can always:

  • EVM→Core: ERC20.transfer(systemBridge, amount) directly (Hyperliquid credits Core from the Transfer event).

  • Core→EVM: use the Hyperliquid UI “Transfer” (Core spotSend → EVM ERC20.transfer from system).
    If you do adopt OFT/Composer, it streamlines user UX (approve + send) and handles EVM dust refunds, but it’s optional for the linking itself.


After HIP‑1 exists and your ERC‑20 is deployed, establish the bridge. Pick proof type (EOA nonce vs storage slot).

  • Composer (CLI):
# Step‑1: Core deployer requests link
npx @layerzerolabs/hyperliquid-composer request-evm-contract \
  --token-index $CORE_INDEX --network mainnet --private-key $CORE_PK

# Step‑2: EVM deployer finalizes link
npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \
  --token-index $CORE_INDEX --network mainnet --private-key $EVM_DEPLOYER_PK
  • SDK (Python): use the full script in Appendix: Linker CLI (Python) (interactive Step‑1/2 + status).

  • Proofs:

    • EOA CREATE: {"create": {"nonce": <deployNonce>}}

    • Factory/CREATE2 or proxies: "firstStorageSlot" or {"customStorageSlot": {slot, expected}} (EIP‑1967 impl slot: 0x3608…2bbc).

  • Critical capacity rule (quoted): “Make sure the asset bridge address on HyperCore has all the tokens minted… Partial funding is not supported.” — LayerZero OFT Guide.

5) Deploy the HyperLiquidComposer (on HyperEVM)

  • What it does (quoted): Composer “transfers received ERC‑20 to the asset bridge and writes a Core spot transfer via CoreWriter; any unbridgeable remainder is refunded as dust.” — LZ Concepts.
# deploy from your repo’s script (example tags)
npx hardhat lz:deploy --tags MyHyperLiquidComposer
  • Activation: Send ≥ $1 HYPE/USDC to the Composer’s implied Core address so it can write Core actions.

6) Send tokens (X‑chain → HyperEVM/Core)

  • EVM → Core (manual): ERC20.transfer(bridge, amount) to 0x2000…<coreIndexHex>; HL credits Core balance from Transfer log.

  • Core → EVM: spotSend to the bridge address (UI has “Transfer”); HL calls ERC20.transfer from system on EVM.

  • Composer path: Use OFT send() with composeMsg set to the Core receiver; Composer handles the rest.

7) (Optional) Add Hyperliquidity (HIP‑2)

  • Register a grid maker later (post‑listing). Use registerHyperliquidity via SDK or a Composer command if available (see dedicated section below).

  • Start with small orderSz, moderate nOrders, and nSeededLevels=0.

<PREP OVERVIEW ENDS>


Composer and network configs to know

Big vs small blocks (HyperEVM gas limits)

“HyperEVM has two kinds of blocks: Small (1s, 2M gas) and Big (1/min, 30M gas). Toggle with an L1 action evmUserModify or via Composer.” — https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts

# LayerZero Composer: switch to Big blocks for deployments
npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PRIVATE_KEY

Precompiles & system contracts (addresses)

0x2222…2222 HYPE system contract,

0x2000…abcd asset bridge (token index = abcd),

0x3333…3333 CoreWriter (writes Core actions from EVM). https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts

Signing guidance

“Use ethers‑v6 signTypedData; v5 EIP‑712 signing is not stable.” https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts

Hyperliquid API signing caveat: lowercase address fields to avoid parse mismatches. https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/signing

UI vs Hyperliquidity default

“Using the Hyperliquid UI for spot deployment forces the use of ‘Hyperliquidity’.

This is NOT supported by LayerZero as it can lead to an uncollateralized asset bridge. Deploy via API/SDK to avoid this.” — https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts


Quick mental model (what’s actually happening)

  • HyperCore (HIP‑1) is the native L1 spot ledger. HyperEVM is an EVM runtime under the same consensus. Linking binds a Core index (HIP‑1) to an ERC‑20 on HyperEVM.

  • After linking, Core→EVM payouts call ERC20.transfer(...) from a per‑token system (vault) address on HyperEVM. EVM→Core credits are inferred from ERC‑20 Transfer logs to that same system address.

  • If your link is non‑canonical, the system address must be pre‑funded on EVM or Core→EVM withdrawals will revert (no logs). If canonical, the bridge mints/burns (no pre‑fund).

Quote (Hyperliquid docs): “Transferring tokens from HyperEVM to HyperCore can be done using an ERC20 transfer with the corresponding system address as the destination.”


Placeholders

  • CORE_TOKEN_INDEX (e.g. 123)

  • EVM_TOKEN_ADDRESS (link the proxy if upgradeable)

  • CORE_WEI_DECIMALS (e.g. 8)

  • EVM_DECIMALS (e.g. 18) → EVM_EXTRA_WEI_DECIMALS = EVM_DECIMALS - CORE_WEI_DECIMALS

  • HYPERLIQUID_NETWORK = mainnet | testnet

  • Secrets: CORE_DEPLOYER_PRIV, EVM_PROOF_PRIV

  • Infra: RPC_URL


0) Environment & safety

Tooling

python3 -m venv .venv && source .venv/bin/activate
pip install hyperliquid eth-account requests web3 python-dotenv
# Optional: Foundry (cast), Node.js if you also use a composer CLI

.env.sample

TESTNET=false
CORE_TOKEN_INDEX=123
EVM_TOKEN_ADDRESS=0x0000000000000000000000000000000000000000
CORE_WEI_DECIMALS=8
EVM_DECIMALS=18
EVM_EXTRA_WEI_DECIMALS=10
CORE_DEPLOYER_PRIV=0xYOUR_CORE_DEPLOYER_KEY
EVM_PROOF_PRIV=0xYOUR_EVM_DEPLOYER_KEY
RPC_URL=https://your-hyperevm-rpc

Safety checklist (what I wish I knew first)

  • Treat ERC‑20 decimals() as immutable post‑link (+ especially post‑genesis).

  • If upgradeable, link the proxy, not the implementation, set admin to a multisig.

  • Signatures use ms timestamps, keep the box NTP‑synced or /exchange rejects.


1) Architecture decisions (decide once, then don’t churn)

1.1 Decimals math

EVM_EXTRA_WEI_DECIMALS = EVM_DECIMALS - CORE_WEI_DECIMALS (e.g., 18 - 8 = 10). You’ll commit this in Step‑1.

Lesson learned: mismatched decimals = broken UX forever. Verify with a real conversion table before linking.

1.2 Canonical vs non‑canonical

  • Canonical: bridge mints/burns on EVM. No pre‑funding of system address.

  • Non‑canonical: bridge uses system (vault) address on EVM to pay withdrawals. You must pre‑fund it.

Lesson learned: our Core→EVM withdrawals failed silently (no ERC‑20 logs) until we funded the vault.

1.3 ERC‑20 flavor

  • CREATE (EOA) → you can use Step‑2 create { nonce } finalize.

  • CREATE2 / factories → prefer a storage proof for Step‑2.

  • Upgradeablelink the proxy and use EIP‑1967 impl slot for storage proof.

Lesson learned: we initially tried create but the token came from a factory path, switching to storage‑slot proof fixed it.


2) Auction → Metadata → Genesis (UI + programmatic)

2.1 Auction (UI)

  1. UI → Spot Deployments / Auction

  2. Bid from the wallet you’ll keep as Core deployer (this key signs Step‑1).

  3. On success, you own CORE_TOKEN_INDEX.

Verify via API

curl -s https://api.hyperliquid.xyz/info \
  -H 'content-type: application/json' -d '{"type":"spot"}' | jq .

The info endpoint is used to fetch information about the exchange… Different request bodies result in different response schemas.

2.2 Metadata (UI or declarative)

  • Set name, fullName, ticker, CORE_WEI_DECIMALS, deployer fee share.

  • Snapshot spotMeta before/after changes; commit JSON to repo.

Composer (optional)

npx @layerzerolabs/hyperliquid-composer core-spot \
  --action get --token-index CORE_TOKEN_INDEX --network HYPERLIQUID_NETWORK
npx @layerzerolabs/hyperliquid-composer core-spot \
  --action create --token-index CORE_TOKEN_INDEX --network HYPERLIQUID_NETWORK
  • Many teams link first (next sections), verify transfers, then click Genesis.

Lesson learned: doing a tiny EVM↔Core round‑trip before Genesis saves you from public mishaps.


3) EVM token (CREATE vs CREATE2, proxy vs non‑proxy)

3.1 CREATE vs CREATE2

  • CREATE: deployed by an EOA; Step‑2 proof can be create { nonce }.

  • CREATE2: deterministic (deployer, salt, init_code); use storage‑based proof.

Helper — precompute CREATE2

from eth_utils import keccak, to_checksum_address

def create2_address(deployer:str, salt_hex:str, init_code:bytes)->str:
    assert salt_hex.startswith('0x') and len(salt_hex)==66
    h = keccak(b'\xff' + bytes.fromhex(deployer[2:]) + bytes.fromhex(salt_hex[2:]) + keccak(init_code))
    return to_checksum_address(h[-20:].hex())

3.2 Upgradeable proxies

  • Always link the proxy.

  • Use Step‑2 storage proof over EIP‑1967 implementation slot.

Read impl slot (Foundry)

cast storage PROXY_ADDRESS \
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

Quote (EIP‑1967): “The address of the logic contract is … saved in a specific storage slot (for example 0x3608…2bbc).”

Lesson learned: we mistakenly tried to link the implementation first; the proxy is the contract users interact with and the one Core should call.


4) Linking Core↔EVM (Step‑1 / Step‑2)

4.1 Values used in both steps

  • token = CORE_TOKEN_INDEX

  • address = EVM_TOKEN_ADDRESS (lowercase in payload)

  • evmExtraWeiDecimals = EVM_DECIMALS - CORE_WEI_DECIMALS

4.2 Step‑1 — requestEvmContract (Core deployer)

Python (signed action)

from hyperliquid.utils import constants
from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action
from eth_account import Account
import requests

TESTNET=False
TOKEN_ID=CORE_TOKEN_INDEX
EVM=EVM_TOKEN_ADDRESS
EVMDIFF=EVM_EXTRA_WEI_DECIMALS
acct=Account.from_key('CORE_DEPLOYER_PRIV')
api=constants.TESTNET_API_URL if TESTNET else constants.MAINNET_API_URL

action={"type":"spotDeploy","requestEvmContract":{
    "token":TOKEN_ID,
    "address":EVM.lower(),
    "evmExtraWeiDecimals":EVMDIFF
}}
nonce=get_timestamp_ms()
sig=sign_l1_action(acct, action, None, nonce, None, not TESTNET)
payload={"action":action,"nonce":nonce,"signature":sig,"vaultAddress":None}
print(requests.post(f"{api}/exchange", json=payload, timeout=30).json())

Composer (alternative)

npx @layerzerolabs/hyperliquid-composer request-evm-contract \
  --token-index CORE_TOKEN_INDEX --network HYPERLIQUID_NETWORK \
  --private-key $CORE_DEPLOYER_PRIV

The exchange endpoint is used to interact with and trade on the Hyperliquid chain.

What can go wrong

  • 400/signature mismatch → wrong isMainnet flag, address not lowercased, or bad clock.

  • Wrong decimals math → you’ll lock in the wrong multiplier.

4.3 Step‑2 — finalizeEvmContract (EVM finalizer)

Pick one proof that matches your deployment.

Option A — Create (EOA + CREATE)

{"type":"finalizeEvmContract","token":CORE_TOKEN_INDEX,
 "input":{"create":{"nonce":DEPLOYER_NONCE}}}

Option B — FirstStorageSlot

{"type":"finalizeEvmContract","token":CORE_TOKEN_INDEX,
 "input":"firstStorageSlot"}

Option C — CustomStorageSlot (e.g., EIP‑1967 impl slot)

{"type":"finalizeEvmContract","token":CORE_TOKEN_INDEX,
 "input":{"customStorageSlot":{"slot":"0x...","expected":"0x..."}}}

Python

from hyperliquid.utils import constants
from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action
from eth_account import Account
import requests

TESTNET=False
TOKEN_ID=CORE_TOKEN_INDEX
acct=Account.from_key('EVM_PROOF_PRIV')
api=constants.TESTNET_API_URL if TESTNET else constants.MAINNET_API_URL

fin_input="firstStorageSlot"  # or {"create":{"nonce":N}} or {"customStorageSlot":{...}}
action={"type":"finalizeEvmContract","token":TOKEN_ID,"input":fin_input}
nonce=get_timestamp_ms()
sig=sign_l1_action(acct, action, None, nonce, None, not TESTNET)
payload={"action":action,"nonce":nonce,"signature":sig,"vaultAddress":None}
print(requests.post(f"{api}/exchange", json=payload, timeout=30).json())

Composer

npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \
  --token-index CORE_TOKEN_INDEX --network HYPERLIQUID_NETWORK \
  --private-key $EVM_PROOF_PRIV

What can go wrong

  • Used create but contract was CREATE2/factory → use storage‑slot proof.

  • Linked implementation instead of proxy → re‑link proxy via storage proof.

curl -s https://api.hyperliquid.xyz/info -H 'content-type: application/json' \
  -d '{"type":"spotMeta"}' | jq '.universe[] | select(.index==CORE_TOKEN_INDEX)'
# Expect: evmContract.address, evm_extra_wei_decimals, weiDecimals, isCanonical

Lesson learned: spotMeta is your source of truth, commit snapshots to the repo anytime you change deploy state.


5) Transfers & the system (vault) address

5.1 Deriving the system address (EVM)

Formula: base 0x2000000000000000000000000000000000000000 + CORE_TOKEN_INDEX → checksum.

from web3 import Web3

def system_address_for(index:int)->str:
    base=int('0x2000000000000000000000000000000000000000',16)
    return Web3.to_checksum_address(hex(base+index))

he tokens are credited to the Core based on the emitted Transfer(address from, address to, uint256 value) from the linked contract.

5.2 EVM → Core

  • Send ERC‑20 to the system address or use the UI. Core reads the Transfer log → credits HIP‑1.

5.3 Core → EVM

  • Bridge calls ERC20.transfer(user, amt) from the system address.

  • Non‑canonical: if vault balance = 0 → EVM transfer reverts (no Transfer logs!).

Fund & inspect (Foundry)

VAULT=$(python - <<'PY'
import os
from web3 import Web3
base=int('0x2000000000000000000000000000000000000000',16)
print(Web3.to_checksum_address(hex(base+int('CORE_TOKEN_INDEX'))))
PY
)
cast send EVM_TOKEN_ADDRESS "transfer(address,uint256)" $VAULT 1000000000000000000 --private-key $FUNDER
cast call EVM_TOKEN_ADDRESS "balanceOf(address)(uint256)" $VAULT

Unit conversions
If CORE_WEI_DECIMALS=8, EVM_DECIMALS=18diff=10.

  • Core→EVM multiplier: 10^10

  • EVM→Core divider: 10^10

Lesson learned: our Core→EVM looked “stuck” until we realized the vault cannot conjure tokens, you must pre‑fund for non‑canonical.


6A: Existing tokens, linking cookbook (no redeploy)

When you already have a token somewhere (either an ERC‑20 on HyperEVM or a HIP‑1 on Core) and you want the two to interoperate.

A. You already have a HIP‑1 on Core, and a separate ERC‑20 on HyperEVM

Goal: Link the existing HIP‑1 index to your existing ERC‑20 (proxy if upgradeable) without redeploying.

Hard requirements

  • Decimals: evmExtraWeiDecimals = (EVM.decimals() − CORE.weiDecimals).

  • Bridge capacity: The token’s system (asset bridge) address must hold the total non‑system supply on the other side before users can bridge into it. Practically, if supply starts on Core, you must pre‑fund the HyperEVM side system address with the amount you want withdrawable to EVM (in EVM units). (See §5 for system address math.)

  • Proxy linking: If upgradeable, link the proxy, not the implementation.

Decision: proof type for Step‑2

  • CREATE by EOA? Use input: { create: { nonce: <deployerNonceAtCreation> } }.

  • Factory/CREATE2 or upgradeable proxy? Use storage proof: "firstStorageSlot" (simple) or {"customStorageSlot": { slot, expected }} with EIP‑1967 implementation slot.

Checklist

  1. Confirm decimals on both sides and compute evmExtraWeiDecimals.

  2. Verify ERC‑20 address is the proxy.

  3. Pre‑fund the HyperEVM system address (vault) with EVM tokens (non‑canonical path) if you expect Core→EVM movement immediately after linking.

  4. Step‑1 (requestEvmContract) from Core deployer with { token, address, evmExtraWeiDecimals }.

  5. Step‑2 (finalizeEvmContract) from EVM finalizer using the right proof (create/storage slot).

  6. spotMeta shows evmContract.address and evm_extra_wei_decimals.

  7. Smoke test: small EVM→Core (ERC‑20 transfer to system address) and Core→EVM (UI or spotSend).

Pre‑funding snippet (Foundry)

VAULT=$(python - <<'PY'
from web3 import Web3
base=int('0x2000000000000000000000000000000000000000',16)
print(Web3.to_checksum_address(hex(base+int(os.environ['CORE_TOKEN_INDEX']))))
PY
)
cast send $EVM_TOKEN_ADDRESS "transfer(address,uint256)" $VAULT 1000000000000000000 --private-key $FUNDER_PK

Composer helpers (CLI)

# compute asset-bridge (system) address for CORE_TOKEN_INDEX
npx @layerzerolabs/hyperliquid-composer to-bridge --token-index $CORE_TOKEN_INDEX
# read spot meta for sanity
npx @layerzerolabs/hyperliquid-composer core-spot --action get --token-index $CORE_TOKEN_INDEX --network mainnet

Linking via SDK (Python)

Linking via Composer (CLI)

# Step‑1
npx @layerzerolabs/hyperliquid-composer request-evm-contract \
  --token-index $CORE_TOKEN_INDEX --network mainnet \
  --private-key $CORE_DEPLOYER_PRIV
# Step‑2 (run with HyperEVM deployer/finalizer key)
npx @layerzerolabs/hyperliquid-composer finalize-evm-contract \
  --token-index $CORE_TOKEN_INDEX --network mainnet \
  --private-key $EVM_PROOF_PRIV

Common pitfalls I hit

  • Using create proof for a factory/CREATE2 deploy → Step‑2 rejected. Fix: storage‑slot proof.

  • Linking implementation address for proxies → bridge never calls user‑visible token. Fix: link proxy; use custom slot proof if needed.

  • Core→EVM “stuck” → vault had 0 balance; pre‑fund.

  • Rounding burn < 1 wei when evmExtraWeiDecimals > 0 → expected per docs.


B. You already have an ERC‑20 on HyperEVM, and want a HIP‑1 on Core

  1. Win/buy the Core index via auction; set token weiDecimals to match your intended bridge math with EVM’s decimals().

  2. Choose canonical vs non‑canonical supply model.

  3. Before linking, decide if you will hold supply on Core or on EVM. If Core holds supply, pre‑mint HIP‑1 to the Core system address; if EVM holds supply, pre‑fund the EVM system address.

  4. Perform Step‑1 / Step‑2 as in A.

  5. Verify with spotMeta, then test transfers both ways.

Smoke tests

  • EVM→Core: ERC20.transfer(system, amt) and check Core balance.

  • Core→EVM: UI Transfer or spotSend to your EVM address and confirm ERC‑20 Transfer from system.


C. Selecting the Step‑2 proof, decision table

Deployment pathRecommended input
EOA deployed with CREATE{ "create": { "nonce": <nonceAtCreation> } }
Factory / CREATE2"firstStorageSlot" (if slot0 holds deployer) or {"customStorageSlot": {slot, expected}}
Upgradeable proxy (EIP‑1967){"customStorageSlot": {slot: 0x3608…2bbc, expected: <impl address padded>}}
You’re unsureStart with firstStorageSlot; if mismatch, read the impl/admin slot and use customStorageSlot

Reading slots (Foundry)

cast storage $PROXY 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

6B) Hyperliquidity (HIP‑2)- (UI + SDK + Composer)

HIP‑2 is an on‑chain market‑making strategy native to Core order books. It creates a geometric grid of resting ALO orders that updates every ~3s; currently USDC‑quoted pairs only.

https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-2-hyperliquidity

Parameters you must set

  • spot (pair index from spotMeta.universe), startPx, orderSz (human units), nOrders, nSeededLevels (0 if no seeding; if you set genesis.noHyperliquidity=true, this must be 0).

Register via SDK (Python)

from eth_account import Account
from hyperliquid.utils.signing import sign_l1_action, get_timestamp_ms
from hyperliquid.utils import constants
import os, requests

TESTNET = os.getenv('TESTNET','false').lower()=='true'
CORE_DEPLOYER_PRIV = os.environ['CORE_DEPLOYER_PRIV']
SPOT_INDEX = int(os.environ['SPOT_INDEX'])  # pair index, not base index
START_PX = os.environ.get('START_PX','0.25')
ORDER_SZ = os.environ.get('ORDER_SZ','1000')  # human units of base
N_ORDERS = int(os.environ.get('N_ORDERS','40'))
N_SEEDED = int(os.environ.get('N_SEEDED_LEVELS','0'))

acct = Account.from_key(CORE_DEPLOYER_PRIV)
api = constants.TESTNET_API_URL if TESTNET else constants.MAINNET_API_URL

body = {"type":"spotDeploy","registerHyperliquidity":{
    "spot": SPOT_INDEX,
    "startPx": START_PX,
    "orderSz": ORDER_SZ,
    "nOrders": N_ORDERS,
    "nSeededLevels": N_SEEDED if N_SEEDED>0 else None
}}

nonce = get_timestamp_ms()
sig = sign_l1_action(acct, body, None, nonce, None, not TESTNET)
payload = {"action": body, "nonce": nonce, "signature": sig, "vaultAddress": None}
print(requests.post(f"{api}/exchange", json=payload, timeout=60).json())

Register via Composer (CLI)

# Ensure the pair exists first (register-spot)
npx @layerzerolabs/hyperliquid-composer register-spot \
  --token-index $BASE_TOKEN_INDEX \
  --network mainnet \
  --private-key $CORE_DEPLOYER_PRIV

# Create a deploy state file (optional)
npx @layerzerolabs/hyperliquid-composer create-spot-deployment \
  --token-index $BASE_TOKEN_INDEX --network mainnet \
  --private-key $CORE_DEPLOYER_PRIV

If your Composer version exposes register-hyperliquidity, prefer that, otherwise use the SDK call above. Credit: LayerZero team.

Operational guidance

  • Start conservative, watch fills/rebalances.

Clearer Additional Section:

Deploying without Hyperliquidity (HIP-2)

Goal: bring your token live on HyperCore + link to your ERC-20 without standing grid-maker orders. This avoids accidental under-collateralization and lets you enable HIP-2 later, intentionally.

Why skip HIP-2 at launch

  • LayerZero’s docs warn that auto-enabling Hyperliquidity (UI path enforces this) can leave the asset bridge under-funded if you haven’t pre-provisioned capacity.

  • Launching without HIP-2 keeps things simple, you can turn it on later once funding and parameters are clear. (Or can engage a MM)

Do this (Composer / SDK path)

Use the Composer/SDK path. The UI will have no way to bypass HIP-2.

# 1) Create deployment state
npx @layerzerolabs/hyperliquid-composer create-spot-deployment \
  --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV

# 2) Write user genesis (balances)
npx @layerzerolabs/hyperliquid-composer user-genesis \
  --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV

# 3) Confirm (locks genesis)
npx @layerzerolabs/hyperliquid-composer set-genesis \
  --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV

# 4) Register the spot pair (USDC-quoted)
npx @layerzerolabs/hyperliquid-composer register-spot \
  --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV

# 5) (INTENTIONALLY OMIT) Do NOT register Hyperliquidity now.
#    Skip any `register-hyperliquidity` or `registerHyperliquidity` calls.

Then proceed to Linking (Step-1/Step-2) as usual:

  • Step-1 (Core): request-evm-contract { token, address, evmExtraWeiDecimals }

  • Step-2 (EVM): finalize-evm-contract { input: create|firstStorageSlot|customStorageSlot }

(Linking script in the Appendix CLI)

If you already enabled Hyperliquidity by accident

Choose one of the following to neutralize it:

A) SDK call with zero orders (disables placements)

# registerHyperliquidity with 0 orders and 0 seeded levels
body = {"type":"spotDeploy","registerHyperliquidity":{
    "spot": SPOT_INDEX,          # pair index (not base index)
    "startPx": "0",              # ignored when nOrders=0
    "orderSz": "0",              # ignored when nOrders=0
    "nOrders": 0,
    "nSeededLevels": 0
}}
# sign & POST via /exchange (see your SDK examples)

B) Composer (if your build exposes it)

npx @layerzerolabs/hyperliquid-composer register-hyperliquidity \
  --token-index $CORE_TOKEN_INDEX \
  --n-orders 0 --n-seeded-levels 0 \
  --network mainnet --private-key $CORE_DEPLOYER_PRIV

In my testing, setting nOrders=0 + nSeededLevels=0 results in no resting ALO orders. If your Composer build doesn’t expose the command, use the SDK call above.

Sanity checks after a no-HIP-2 launch

  • Orderbook: there should be no Hyperliquidity grid on your pair (check in the UI, your deployer should show 0 open ALO orders).

  • Transfers: run canaries both ways:

    • EVM → Core: ERC20.transfer(systemAddress, amt) → Core balance increases.

    • Core → EVM: UI “Transfer” or spotSend → ERC-20 Transfer from the system/vault address.

  • Bridge capacity: if non-canonical, pre-fund the destination system/vault address before inviting users to bridge (REITERATING)


6C) Composer mechanics (what it actually does)

LZ Concepts: Composer takes ERC‑20 on HyperEVM, transfers amounts.evm to the asset bridge 0x2000…abcd, then performs a CoreWriter action to sendSpot(receiver, coreIndexId, amounts.core) on HyperCore.

Any unbridgeable remainder is refunded as dust on EVM. Invariant: amounts.dust + amounts.evm = amountLD and amounts.evm = 10^decimalDiff * amounts.core. https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts

Decimals & scaling
Composer handles HIP‑1↔ERC‑20 decimal differences automatically when moving funds (you still must set evmExtraWeiDecimals correctly during linking).

Receiver routing
The EVM call encodes the Core receiver in SendParam.composeMsg, the Composer’s lzCompose unpacks it and writes the Core spot transfer on your behalf.


6D) Doing it without SDK, raw HTTP pattern

You’ll still need a signer, the raw /exchange payload is the same, but you must provide signature yourself.

Pattern

  1. Build the action JSON exactly as shown in §4 and §6B.

  2. EIP‑712 sign with your EVM key (same key you would pass to SDK). The Python SDK’s sign_l1_action is the canonical reference, reproduce it in your stack if avoiding the SDK.

  3. POST to /exchange with body { action, nonce, signature, vaultAddress }.

Hybrid approach: use Python just to sign & emit payload.json, then curl it.

python sign_only.py > payload.json
curl -sS https://api.hyperliquid.xyz/exchange \
  -H 'content-type: application/json' -d @payload.json | jq .

6D) Existing token teams

  1. Audit decimals and set evmExtraWeiDecimals.

  2. Proxy correctness: link the proxy and store the implementation slot if you’re using a proxy pattern; pick customStorageSlot for Step‑2.

  3. Pre‑fund bridge capacity where the circulating starts (Core or EVM) — the system address on the destination must have enough to pay out.

  4. Step‑1 with Core deployer → Step‑2 with EVM finalizer.

  5. Verify spotMeta and run 1‑unit canaries both directions.

  6. If adding Hyperliquidity, set modest params and watch live fills before scaling.

Our own experience

  • Wrong proof chosen → Step‑2 failed silently, switching to customStorageSlot with EIP‑1967 fixed it.

  • Empty vault → Core→EVM appeared “lost” (no logs) until we funded the vault.

  • Pasted Python into bash → “syntax error near unexpected token from’”. Save and run with python3`.


6E) Reference commands & queries

# Info → spot meta (filter by index)
curl -s https://api.hyperliquid.xyz/info -H 'content-type: application/json' \
  -d '{"type":"spotMeta"}' | jq '.universe[] | select(.index==ENV.CORE_TOKEN_INDEX)'

# Compute asset-bridge address (Composer)
npx @layerzerolabs/hyperliquid-composer to-bridge --token-index $CORE_TOKEN_INDEX

# Step‑1 / Step‑2 (Composer)
npx @layerzerolabs/hyperliquid-composer request-evm-contract --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV
npx @layerzerolabs/hyperliquid-composer finalize-evm-contract --token-index $CORE_TOKEN_INDEX --network mainnet --private-key $EVM_PROOF_PRIV

# Register spot pair (Composer)
npx @layerzerolabs/hyperliquid-composer register-spot --token-index $BASE_TOKEN_INDEX --network mainnet --private-key $CORE_DEPLOYER_PRIV

6F) What to put in your repo

/docs/README.md                     # this runbook
/deploy/hyperliquidity.json         # HIP‑2 params (startPx, nOrders, orderSz, nSeededLevels)
/scripts/hl_linker.py               # linker CLI (Step‑1/2 + vault inspect)
/scripts/hip2_register.py           # registerHyperliquidity (SDK)
/scripts/helpers/read_slot.sh       # cast storage helper for EIP‑1967

Appendix — Linker CLI

Reference implementation for Step‑1 / Step‑2 and status checks. Replace placeholders before running. Save as scripts/hl_linker.py, then run:

python3 scripts/hl_linker.py

from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from hyperliquid.utils.signing import get_timestamp_ms, sign_l1_action
from eth_account import Account
import json
import requests

# Configuration
TESTNET = False
TOKEN_ID = int(os.getenv("CORE_TOKEN_INDEX", "123"))
EVM_CONTRACT_ADDRESS = "<add yours>" 
EVM_EXTRA_WEI_DECIMALS = 10
EVM_DEPLOYMENT_NONCE = 1


def main():
    print("🔗 Hyperliquid Token Linking Tool")
    print("=" * 50)

    # Choose which step to perform
    print("
📋 Available actions:")
    print("1. Request EVM Contract (Step 1 - HyperCore token deployer)")
    print("2. Finalize EVM Contract (Step 2 - EVM contract deployer)")
    print("3. Check token status")

    choice = input("
🤔 Which action do you want to perform? (1/2/3): ").strip()

    if choice == "1":
        request_evm_contract()
    elif choice == "2":
        finalize_evm_contract()
    elif choice == "3":
        check_token_status()
    else:
        print("❌ Invalid choice")

def request_evm_contract():
    """Step 1: HyperCore token deployer requests to link to EVM contract"""
    print("
🚀 Step 1: Request EVM Contract Link")
    print("-" * 40)

    # Get private key
    private_key = input("🔑 Enter the PRIVATE KEY of the HyperCore token deployer: ").strip()

    if not private_key.startswith("0x"):
        private_key = "0x" + private_key

    try:
        # Create account
        account = Account.from_key(private_key)
        print(f"👤 Using address: {account.address}")

        # Choose API URL
        api_url = constants.TESTNET_API_URL if TESTNET else constants.MAINNET_API_URL
        print(f"
📡 Connecting to: {api_url}")
        print(f"🪙 Token ID: {TOKEN_ID}")
        print(f"📄 EVM Contract: {EVM_CONTRACT_ADDRESS}")
        print(f"🔢 Wei Decimals Difference: {EVM_EXTRA_WEI_DECIMALS}")

        # Confirm before proceeding
        confirm = input("
⚠️  Proceed with requestEvmContract? (y/N): ").strip().lower()
        if confirm != 'y':
            print("❌ Cancelled")
            return

        # Create the action
        print("
🔄 Sending requestEvmContract...")

        action = {
            "type": "spotDeploy",
            "requestEvmContract": {
                "token": TOKEN_ID,
                "address": EVM_CONTRACT_ADDRESS.lower(),
                "evmExtraWeiDecimals": EVM_EXTRA_WEI_DECIMALS,
            },
        }

        # Sign and send
        nonce = get_timestamp_ms()
        signature = sign_l1_action(
            account, 
            action, 
            None,  # vaultAddress
            nonce, 
            None,  # expiresAfter
            not TESTNET  # isMainnet
        )

        payload = {
            "action": action,
            "nonce": nonce,
            "signature": signature,
            "vaultAddress": None,
        }

        response = requests.post(f"{api_url}/exchange", json=payload)
        result = response.json()

        print(f"
✅ Response: {json.dumps(result, indent=2)}")

        if result.get("status") == "ok":
            print("
🎉 SUCCESS! Step 1 completed.")
            print("👉 Now the EVM contract deployer needs to run Step 2")
        else:
            print(f"
❌ ERROR: {result}")

    except Exception as e:
        print(f"
💥 Error: {e}")

def finalize_evm_contract():
    """Step 2: EVM contract deployer finalizes the link"""
    print("
🏁 Step 2: Finalize EVM Contract Link")
    print("-" * 40)

    # Get private key
    private_key = input("🔑 Enter the PRIVATE KEY of the EVM contract deployer: ").strip()

    if not private_key.startswith("0x"):
        private_key = "0x" + private_key

    try:
        # Create account
        account = Account.from_key(private_key)
        print(f"👤 Using address: {account.address}")

        # Choose API URL
        api_url = constants.TESTNET_API_URL if TESTNET else constants.MAINNET_API_URL
        print(f"
📡 Connecting to: {api_url}")
        print(f"🪙 Token ID: {TOKEN_ID}")
        print(f"🔢 EVM Deployment Nonce: {EVM_DEPLOYMENT_NONCE}")

        # Choose finalization method
        print("
🤔 Choose finalization method:")
        print("1. Using deployment nonce (recommended)")
        print("2. Using first storage slot")

        method = input("Choice (1/2): ").strip()

        if method == "1":
            finalize_input = {"create": {"nonce": EVM_DEPLOYMENT_NONCE}}
        else:
            finalize_input = "firstStorageSlot"

        # Confirm before proceeding
        print(f"
📋 Finalization input: {finalize_input}")
        confirm = input("⚠️  Proceed with finalizeEvmContract? (y/N): ").strip().lower()
        if confirm != 'y':
            print("❌ Cancelled")
            return

        # Create the action
        print("
🔄 Sending finalizeEvmContract...")

        action = {
            "type": "finalizeEvmContract",
            "token": TOKEN_ID,
            "input": finalize_input,
        }

        # Sign and send
        nonce = get_timestamp_ms()
        signature = sign_l1_action(
            account, 
            action, 
            None,  # vaultAddress
            nonce, 
            None,  # expiresAfter
            not TESTNET  # isMainnet
        )

        payload = {
            "action": action,
            "nonce": nonce,
            "signature": signature,
            "vaultAddress": None,
        }

        response = requests.post(f"{api_url}/exchange", json=payload)
        result = response.json()

        print(f"
✅ Response: {json.dumps(result, indent=2)}")

        if result.get("status") == "ok":
            print("
🎉 SUCCESS! Token linking completed!")
            print("👉 Your HyperCore and HyperEVM tokens are now linked!")
        else:
            print(f"
❌ ERROR: {result}")

    except Exception as e:
        print(f"
💥 Error: {e}")

def check_token_status():
    """Check the current status of the token"""
    print("
🔍 Checking Token Status")
    print("-" * 30)

    try:
        # Get spot metadata
        api_url = constants.MAINNET_API_URL
        response = requests.post(f"{api_url}/info", json={"type": "spotMeta"})
        spot_meta = response.json()

        # Find our token
        token_info = None
        for token in spot_meta.get("universe", []):
            if token.get("index") == TOKEN_ID:
                token_info = token
                break

        if token_info:
            print(f"
📊 Token {TOKEN_ID} Status:")
            print(f"   Name: {token_info.get('name', 'N/A')}")
            print(f"   Full Name: {token_info.get('fullName', 'N/A')}")
            print(f"   Token ID: {token_info.get('tokenId', 'N/A')}")
            print(f"   EVM Contract: {token_info.get('evmContract', 'null (not linked)')}")
            print(f"   Is Canonical: {token_info.get('isCanonical', False)}")

            if token_info.get('evmContract'):
                print("
✅ Token is LINKED to EVM!")
                print(f"🔗 EVM Contract Address: {token_info.get('evmContract')}")
            else:
                print("
⏳ Token is NOT YET linked to EVM")
        else:
            print(f"
❌ Token {TOKEN_ID} not found")

    except Exception as e:
        print(f"
💥 Error checking status: {e}")

if __name__ == "__main__":
    main()

Official examples: Hyperliquid’s transfer doc links a sample evm_erc20.py (ERC‑20 deploy + link). The file is served from their GitBook: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyperevm-transfers

Curl & Foundry cheats

# spotMeta universe (filter your index)
curl -s https://api.hyperliquid.xyz/info -H 'content-type: application/json' -d '{"type":"spotMeta"}' | jq '.universe[] | select(.index==CORE_TOKEN_INDEX)'

# spot program / auction
curl -s https://api.hyperliquid.xyz/info -H 'content-type: application/json' -d '{"type":"spot"}' | jq .

System Address Math & Helpers

  • Definition: “Every token has a system address on Core with first byte 0x20 and last bytes the big‑endian token index.” - HL Transfers.

  • Compute (Python):

from web3 import Web3
CORE_INDEX=1234
addr_int = int('20' + '00'*18,16) + CORE_INDEX  # 0x20.. + index
print(Web3.to_checksum_address(hex(addr_int)))

Composer internals

“Transfers the received ERC‑20 to the asset bridge and writes a Core spot transfer via CoreWriter; any unbridgeable remainder is refunded.” — LayerZero Hyperliquid Docs.

CLI you’ll use a lot: set-block, create-spot-deployment, user-genesis, set-genesis, register-spot, trading-fee, request-evm-contract, finalize-evm-contract.


Real errors (and fixes)

  1. Tokens “lost” EVM→Core, Core bridge wasn’t fully funded. Fix: pre‑fund destination system address (capacity is not enforced by HL; Composer only refunds dust on EVM).

  2. Finalize failed with factory/CREATE2 or proxies, use storage‑slot proof (firstStorageSlot / customStorageSlot EIP‑1967) instead of create { nonce }.

  3. Decimal mismatch dust, Wrong evmExtraWeiDecimals burns <1 wei by design. Compute EVM.decimals − CORE.weiDecimals exactly.

  4. EIP‑712 signing (ethers v5): Use ethers v6 signTypedData per LZ guidance.

  5. Casing issues: Lowercase all address fields for HL signed actions.

  6. “User or API Wallet does not exist”: Activate the account on Core (≥ $1 USDC/HYPE) before L1 actions.

  7. Composer deploy reverts on small blocks: Switch to big blocks (30M gas) for deploy.

Auction/Metadata

  • Wrong wallet won → keep the same key for Step‑1, otherwise handover ownership.

Step‑1 (requestEvmContract)

  • 400 / signature mismatch → ensure isMainnet=(not TESTNET), lowercase EVM_TOKEN_ADDRESS, nonce is ms timestamp; sync clock.

  • Decimals mismatchevmExtraWeiDecimals must equal EVM_DECIMALS - CORE_WEI_DECIMALS.

Step‑2 (finalizeEvmContract)

  • Used create but was CREATE2/factory → switch to firstStorageSlot or customStorageSlot.

  • Linked implementation instead of proxy → re‑link proxy using storage‑slot proof.

Verify

  • spotMeta shows no evmContract → Step‑2 not accepted; retry with correct proof.

Transfers

  • Core→EVM reverts, no Transfer logs; from=0x20..+indexempty vault; fund the system address.

  • EVM→Core doesn’t credit → must send to system address; confirm a Transfer event exists.

Local dev pitfalls (I hit these)

  • Ran Python code in shell by mistake → save to hl_linker.py and run python3 hl_linker.py (don’t paste into bash).

  • Forgetting to pip install hyperliquidModuleNotFoundError; install into the virtualenv.


References

If this guide saved you a headache or two, you can always buy me a coffee (in HYPE). HL / HyperEVM address: 0x21325bb9cc109f096c03F4c7eCCeC8E5FCe3Ed49.

0
Subscribe to my newsletter

Read articles from Zeel Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Zeel Patel
Zeel Patel