The Hyperliquid Runbook

Table of contents
- Critical warnings (read before linking)
- Runbook
- UI vs SDK, which to use
- 1) Buy a Core Spot index
- 2) Deploy HIP‑1 (Core)
- 3) Deploy (or reuse) your ERC‑20 on HyperEVM (OFT, plain ERC‑20, or other bridges)
- 4) Link HIP‑1 ↔ ERC‑20 (asset bridge)
- 5) Deploy the HyperLiquidComposer (on HyperEVM)
- 6) Send tokens (X‑chain → HyperEVM/Core)
- 7) (Optional) Add Hyperliquidity (HIP‑2)
- Composer and network configs to know
- Quick mental model (what’s actually happening)
- Placeholders
- 0) Environment & safety
- 1) Architecture decisions (decide once, then don’t churn)
- 2) Auction → Metadata → Genesis (UI + programmatic)
- 3) EVM token (CREATE vs CREATE2, proxy vs non‑proxy)
- 4) Linking Core↔EVM (Step‑1 / Step‑2)
- 5) Transfers & the system (vault) address
- 6A: Existing tokens, linking cookbook (no redeploy)
- 6B) Hyperliquidity (HIP‑2)- (UI + SDK + Composer)
- Clearer Additional Section:
- Deploying without Hyperliquidity (HIP-2)
- 6C) Composer mechanics (what it actually does)
- 6D) Doing it without SDK, raw HTTP pattern
- 6D) Existing token teams
- 6E) Reference commands & queries
- 6F) What to put in your repo
- Appendix — Linker CLI
- System Address Math & Helpers
- Composer internals
- Real errors (and fixes)
- References

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
UI: https://app.hyperliquid.xyz/deploySpot (31‑hour Dutch auction, pay to mint the index).
CLI reference: You won’t buy via CLI, just record the Core token index after auction.
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
… max18446744073709551615
.” “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:
the ERC‑20 emits standard
Transfer
events, andyou 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 emitTransfer(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:
Confirm
decimals()
and computeevmExtraWeiDecimals = EVM.decimals − CORE.weiDecimals
.If upgradeable, identify the proxy and plan a custom storage‑slot proof (EIP‑1967 implementation slot) for Step‑2.
Ensure the token doesn’t restrict transfers from the asset‑bridge (system) address; no blacklists/pauses.
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
orcustomStorageSlot
proof.Proxy (UUPS/Transparent) → link proxy; use
customStorageSlot
with EIP‑1967implementation
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 theTransfer
event).Core→EVM: use the Hyperliquid UI “Transfer” (Core
spotSend
→ EVMERC20.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.
4) Link HIP‑1 ↔ ERC‑20 (asset bridge)
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)
to0x2000…<coreIndexHex>
; HL credits Core balance fromTransfer
log.Core → EVM:
spotSend
to the bridge address (UI has “Transfer”); HL callsERC20.transfer
from system on EVM.Composer path: Use OFT
send()
withcomposeMsg
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
, moderatenOrders
, andnSeededLevels=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-conceptsHyperliquid 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‑20Transfer
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.
Upgradeable → link 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)
UI → Spot Deployments / Auction
Bid from the wallet you’ll keep as Core deployer (this key signs Step‑1).
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
2.3 Genesis (UI recommended)
- 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.
4.4 Verify the link
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=18
→ diff=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
Confirm decimals on both sides and compute
evmExtraWeiDecimals
.Verify ERC‑20 address is the proxy.
Pre‑fund the HyperEVM system address (vault) with EVM tokens (non‑canonical path) if you expect Core→EVM movement immediately after linking.
Step‑1 (
requestEvmContract
) from Core deployer with{ token, address, evmExtraWeiDecimals }
.Step‑2 (
finalizeEvmContract
) from EVM finalizer using the right proof (create/storage slot).spotMeta
showsevmContract.address
andevm_extra_wei_decimals
.Smoke test: small EVM→Core (ERC‑20
transfer
to system address) and Core→EVM (UI orspotSend
).
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
Win/buy the Core index via auction; set token
weiDecimals
to match your intended bridge math with EVM’sdecimals()
.Choose canonical vs non‑canonical supply model.
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.
Perform Step‑1 / Step‑2 as in A.
Verify with
spotMeta
, then test transfers both ways.
Smoke tests
EVM→Core:
ERC20.transfer(system, amt)
and check Core balance.Core→EVM: UI
Transfer
orspotSend
to your EVM address and confirm ERC‑20Transfer
from system.
C. Selecting the Step‑2 proof, decision table
Deployment path | Recommended 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 unsure | Start 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.
Parameters you must set
spot
(pair index fromspotMeta.universe
),startPx
,orderSz
(human units),nOrders
,nSeededLevels
(0 if no seeding; if you setgenesis.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-20Transfer
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 bridge0x2000…abcd
, then performs a CoreWriter action tosendSpot(receiver, coreIndexId, amounts.core)
on HyperCore.Any unbridgeable remainder is refunded as dust on EVM. Invariant:
amounts.dust + amounts.evm = amountLD
andamounts.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 providesignature
yourself.
Pattern
Build the
action
JSON exactly as shown in §4 and §6B.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.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
Audit decimals and set
evmExtraWeiDecimals
.Proxy correctness: link the proxy and store the implementation slot if you’re using a proxy pattern; pick
customStorageSlot
for Step‑2.Pre‑fund bridge capacity where the circulating starts (Core or EVM) — the system address on the destination must have enough to pay out.
Step‑1 with Core deployer → Step‑2 with EVM finalizer.
Verify
spotMeta
and run 1‑unit canaries both directions.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)
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).
Finalize failed with factory/CREATE2 or proxies, use storage‑slot proof (
firstStorageSlot
/customStorageSlot
EIP‑1967) instead ofcreate { nonce }
.Decimal mismatch dust, Wrong
evmExtraWeiDecimals
burns <1 wei by design. ComputeEVM.decimals − CORE.weiDecimals
exactly.EIP‑712 signing (ethers v5): Use ethers v6
signTypedData
per LZ guidance.Casing issues: Lowercase all address fields for HL signed actions.
“User or API Wallet does not exist”: Activate the account on Core (≥ $1 USDC/HYPE) before L1 actions.
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)
, lowercaseEVM_TOKEN_ADDRESS
, nonce is ms timestamp; sync clock.Decimals mismatch →
evmExtraWeiDecimals
must equalEVM_DECIMALS - CORE_WEI_DECIMALS
.
Step‑2 (finalizeEvmContract
)
Used
create
but was CREATE2/factory → switch tofirstStorageSlot
orcustomStorageSlot
.Linked implementation instead of proxy → re‑link proxy using storage‑slot proof.
Verify
spotMeta
shows noevmContract
→ Step‑2 not accepted; retry with correct proof.
Transfers
Core→EVM reverts, no
Transfer
logs;from=0x20..+index
→ empty 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 runpython3 hl_linker.py
(don’t paste into bash).Forgetting to
pip install hyperliquid
→ModuleNotFoundError
; install into the virtualenv.
References
Core Concepts (LZ): warnings, blocks, precompiles, linking, Composer internals — https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-concepts
Hyperliquid SDK (LZ): Composer CLI commands — https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-sdk
OFT on Hyperliquid (LZ): step‑by‑step, UI vs SDK, checklist — https://docs.layerzero.network/v2/developers/hyperliquid/hyperliquid-oft-deployment
Hyperliquid Transfers (HL): system address, transfer model, finalize proofs — https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/hypercore-less-than-greater-than-hyperevm-transfers
HL API:
/info spotMeta
and/exchange
— https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint , https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpointSigning tips (HL): lowercase addresses — https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/signing
If this guide saved you a headache or two, you can always buy me a coffee (in HYPE). HL / HyperEVM address: 0x21325bb9cc109f096c03F4c7eCCeC8E5FCe3Ed49
.
Subscribe to my newsletter
Read articles from Zeel Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
