Elysium

Token bridging

The issuer ceremony for taking a token to HyperCore: buying the ticker at the spot-deploy auction, creating the deposit wallet, genesis, linking, and listing on a spot orderbook.

Last updated

An issuer's guide to taking a HyperEVM token to HyperCore: buy the ticker at the spot-deploy auction, create its deposit wallet, genesis the Core token, link it to the wallet, and list it on a Core spot orderbook.

This one-time ceremony comes before HyperCore bridging via deposit wallets, which covers the runtime deposit and withdraw flow once the link is finalized. An Elysium-native token first arrives on HyperEVM as a mirror and then runs this exact ceremony on the mirror. See Token bridging on Elysium for how the legs compose.

The Hyperliquid side of this ceremony (the auction, genesis, linking, and listing) runs on testnet and mainnet today; only the API endpoints differ. The deposit-wallet contracts are pre-launch, so the factory address is published at launch and this guide walks a hypothetical deployment (ticker ABC, Core token index 1234) with named constants for every address.

Overview

Spot deployment is a sequence of deployer actions submitted to the HyperCore exchange endpoint, plus one contract call on HyperEVM:

plaintext
ticker won → deposit wallet created → genesis staged → genesis done
           → link requested → linked (deposits open) → listed on a spot orderbook
  • Steps 1–5 (planning through linking) are what bridging requires. Once the link finalizes, deposits and withdrawals work.
  • Step 6 (listing) is optional and can wait. Until it runs, explorers and deploy UIs show the token as a pending token, but the wallet, the link, and balances are unaffected.
  • In practice the ceremony spans days because of auction and announcement timing. There is no deadline between winning the ticker and the later steps.

Only the deployer, the address that won the ticker, can perform these actions for that token. Link requests are per ticker and deployer-gated, so no third party can link or list against your ticker. The whole path is also permissionless: every action is signed by the deployer alone, with no allowlist, review, or third-party approval between buying the ticker and a live orderbook.

The steps at a glance

  1. Plan decimals and supply. The decimal spec is locked forever when you buy the ticker, so decide it first.
  2. Buy the ticker at the spot-deploy auction.
  3. Create the deposit wallet on HyperEVM. The wallet is the contract HyperCore links to, so your token needs no bridge code. Create it while the ticker is still unlinked, with hyperCoreDeployer set to the key that will sign the link.
  4. Genesis: stage with userGenesis, finalize with genesis. One-shot and irreversible. maxSupply must equal the staged sum, and for a bridged token the system address must hold the full supply.
  5. Link: requestEvmContract, then finalizeEvmContract. Genesis must come first, the finalize signature must come from the key in the wallet's finalizer slot, and the link is permanent. Bridging is live from here.
  6. List: enable your quote asset if it isn't USDC, then registerSpot, then registerHyperliquidity, and verify the pair in spotMeta.universe. registerSpot alone does not list, and nOrders must be 0 for a token with Hyperliquidity disabled.

Dry-run first. Build and print the exact action dict before signing anything. genesis and finalizeEvmContract are one-shot, and the printed JSON is exactly what the signature covers.

Two signing families. Every action in this guide is an exchange L1 action, signed with the Python SDK's sign_l1_action. That is a different scheme from the user-signed HyperliquidTransaction:* family used by sendAsset. Use the SDK for both; it is the reference encoder, and hand-rolling the wrong family produces signature errors.

All Python below assumes the official Python SDK:

python
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from eth_account import Account

# TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz"
# MAINNET_API_URL = "https://api.hyperliquid.xyz"
deployer = Account.from_key(DEPLOYER_KEY)   # the auction-winning key
exchange = Exchange(deployer, base_url=API_URL)
info = Info(base_url=API_URL, skip_ws=True)

Example deployment (hypothetical)

The same hypothetical deployment as the bridging guide. For a real token, every row is readable live from spotMeta and tokenDetails.

Value

HyperCore ticker

ABC (hypothetical)

Core token index

1234 (szDecimals 2, weiDecimals 8)

Core token id

<tokenId> (assigned at registration; from spotMeta)

System address (0x20‖1234)

0x20000000000000000000000000000000000004d2

Underlying ERC-20 (HyperEVM)

UNDERLYING (decimals 8)

Deposit wallet (the linked evmContract)

DEPOSIT_WALLET

Max supply

1,000,000,000 tokens

evmExtraWeiDecimals

0 (underlying decimals 8 − weiDecimals 8)

Step 1: Plan decimals and supply

Core amounts are uint64 in Core wei (weiDecimals places). The EVM link scales by evmExtraWeiDecimals = underlyingDecimals − weiDecimals, which must be in [-2, 18] and is fixed forever once the link finalizes:

Underlying decimals()

weiDecimals

evmExtraWeiDecimals

8

8

0 (units map 1:1)

6 (USDC-style)

8

-2

18

8

10

The ticker spec also requires szDecimals + 5 <= weiDecimals. For a token bridging from an 8-decimal underlying, weiDecimals 8 is the natural choice.

Sub-weiDecimals remainders on transfers into Core are burned. The whole genesis supply must fit uint64 Core wei with headroom: pick a real max supply, never exactly 2^64 − 1, which can trip an internal overflow guard at genesis. For a token bridging through a deposit wallet, max supply should cover the underlying's entire EVM supply, since the system address must be able to back every possible deposit. Worked conversions are in Amounts and decimals.

All amounts in the deployer actions below are Core-wei strings.

Step 2: Buy the ticker

Tickers are sold in a recurring Dutch auction (≈31-hour cycles, with the price descending to a floor). Winning registerToken2 locks the ticker name and the token's decimal spec. Supply does not exist yet, and nothing else about the token is committed.

The live auction, with current price and recent ticker sales, is visible on the hypurrscan dashboard in the token-auction section. Auction cadence, the pricing curve, and current gas are best read live and from the official docs: Deploying HIP-1 and HIP-2 assets and HIP-1.

The spec is permanent: name is at most 6 characters, and the decimals from step 1 cannot change after purchase.

python
# Live auction state for your deployer address (also shows your ceremony progress later)
state = info.post("/info", {"type": "spotDeployState", "user": deployer.address})

exchange.spot_deploy_register_token(
    TICKER_NAME,       # <= 6 chars, e.g. "ABC"
    SZ_DECIMALS,       # e.g. 2
    WEI_DECIMALS,      # e.g. 8
    MAX_GAS,           # your auction bid ceiling; reverts if the current price exceeds it
    FULL_NAME,         # display name, e.g. "ABC Token"
)

After winning, spotDeployState shows the spec locked with maxSupply: null, zero genesis balances, and no spot pair. That is the expected state until genesis.

Step 3: Create the deposit wallet

The contract HyperCore links to is a deposit wallet, the escrow adapter described in HyperCore bridging via deposit wallets. The wallet, not your ERC-20, is what HyperCore links to, which is why any existing token graduates with zero token-code changes. This is the same factory whose walletFor and predictWallet getters resolve wallets; creation is the one issuer write on the same (underlying, tickerIndex, hyperCoreDeployer) tuple.

solidity
// DEPOSIT_WALLET_FACTORY: published at launch.
interface IHyperCoreDepositFactory {
    function createWallet(address underlying, uint64 tickerIndex, address hyperCoreDeployer)
        external returns (address wallet);
    function predictWallet(address underlying, uint64 tickerIndex, address hyperCoreDeployer)
        external view returns (address);  // deterministic address, deployed or not
    function walletFor(address underlying, uint64 tickerIndex, address hyperCoreDeployer)
        external view returns (address);  // deployed address, or 0x0 if none
}
solidity
IHyperCoreDepositFactory factory = IHyperCoreDepositFactory(DEPOSIT_WALLET_FACTORY);
address predicted = factory.predictWallet(UNDERLYING, TICKER_INDEX, HYPERCORE_DEPLOYER);
address wallet = factory.createWallet(UNDERLYING, TICKER_INDEX, HYPERCORE_DEPLOYER);
// wallet == predicted, and walletFor(...) now returns it

The parameters:

  • underlying: the ERC-20 the wallet escrows. For an Elysium-native token, this is its mirror on HyperEVM.
  • tickerIndex: the Core token index from step 2.
  • hyperCoreDeployer: the address that will later sign finalizeEvmContract. The factory writes it into the wallet's keccak256("HyperCore deployer") slot, which is exactly what the customStorageSlot link proof checks in step 5. For most issuers it is the auction deployer key itself.

Constraints and timing:

  • Create the wallet while the ticker is still unlinked. Creation validates the ticker against live Core state and requires evmContract == null. HYPE's own index is rejected outright, because native HYPE bridges through its own system address, never a wallet.
  • Creation is permissionless and moves no funds. Anyone can deploy the wallet for a (token, ticker, finalizer) tuple, and the caller gains no rights; the finalizer key is what matters.
  • Ordering is otherwise free: the wallet can be created before or after genesis. predictWallet even supports requesting the link against the predicted address and deploying later, but the simplest order is create, then genesis, then link.

Step 4: Genesis

Genesis mints the Core supply. For a bridged token, all of it goes to the token's system address (0x2000…0000 | tokenIndex) so that Core credits are always backed by EVM escrow. Hyperliquidity is disabled, since HIP-2 seeding requires deployer USDC per level and targets fresh launches rather than assets with EVM-side price discovery.

Two actions, in order: stage the balances, then finalize.

python
# 1) Stage: park the full supply at the system address
exchange.spot_deploy_user_genesis(
    TICKER_INDEX,
    [(SYSTEM_ADDRESS, str(MAX_SUPPLY_WEI))],   # amounts are Core-wei strings
    [],                                         # no proportional airdrop from an existing token
)

# 2) Finalize: total must equal the staged sum; no_hyperliquidity=True for bridged tokens
exchange.spot_deploy_genesis(TICKER_INDEX, str(MAX_SUPPLY_WEI), True)

userGenesis is more general than the bridged-token pattern above and can be called multiple times to stage allocations before the one-shot genesis seals them:

  • userAndWei is a list of [address, coreWei] allocations to any recipients, not only the system address. A non-bridged launch can genesis straight to holders.
  • existingTokenAndWei stages a proportional airdrop to an existing Core token's holders ([tokenIndex, coreWei] per anchor token).
  • An optional blacklistUsers field excludes addresses from an anchor-token airdrop.
  • The staged total must fit uint64, and genesis.maxSupply must equal it exactly. Scale human amounts by the token's live weiDecimals (from spotMeta), never by the EVM decimals().

For a bridged token, keep everything at the system address. Any supply minted elsewhere is Core supply not backed by EVM escrow, which breaks the wallet invariant.

genesis is one-shot and irreversible. Preview your inputs carefully, and never re-run it to clear a pending token status later. Pending means the listing step hasn't run, not that genesis failed (deploy-UI "trigger genesis" buttons don't observe API-submitted genesis).

On the wire, noHyperliquidity is present only when true. The SDK omits the key otherwise, and the signature covers the exact bytes. The action shape (values illustrative, amounts in Core wei):

json
{"type": "spotDeploy", "genesis": {"token": 1234, "maxSupply": "100000000000000000", "noHyperliquidity": true}}

Verify that the system address now holds the max supply:

python
bal = info.post("/info", {"type": "spotClearinghouseState", "user": SYSTEM_ADDRESS})
# → balances contain the token at exactly MAX_SUPPLY_WEI

Linking binds the Core token to its HyperEVM contract: the deposit wallet, not the underlying ERC-20. It takes two deployer actions, request and then finalize.

Genesis must come before finalize. Core does not check that the system address is funded when the link finalizes, so a linked but unfunded token would open deposits against an empty system address. Always fund through genesis first.

The link is permanent. A wrong token index, wallet address, or evmExtraWeiDecimals cannot be corrected. Verify each input against the live wallet before finalizing.

The SDK has no wrapper for these two actions, so sign and submit them as raw L1 actions with the SDK's own signer:

python
import time, requests
from hyperliquid.utils.signing import sign_l1_action

def submit(action: dict) -> dict:
    nonce = int(time.time() * 1000)
    sig = sign_l1_action(deployer, action, None, nonce, None, IS_MAINNET)
    r = requests.post(f"{API_URL}/exchange", json={
        "action": action, "nonce": nonce, "signature": sig,
        "vaultAddress": None, "expiresAfter": None,
    }, timeout=30)
    return r.json()

# 1) Request: nominate the EVM contract. Nested under spotDeploy is the
#    canonical form; {"type": "requestEvmContract", ...} also exists as a
#    standalone top-level fallback with the same inner fields.
submit({"type": "spotDeploy", "requestEvmContract": {
    "token": TICKER_INDEX,
    "address": DEPOSIT_WALLET.lower(),
    "evmExtraWeiDecimals": EVM_EXTRA_WEI_DECIMALS,
}})

# 2) Finalize: prove control of the EVM contract
submit({"type": "finalizeEvmContract",    # note: top-level, not nested under spotDeploy
        "token": TICKER_INDEX,
        "input": "customStorageSlot"})

finalizeEvmContract proves control of the EVM contract with one of three variants:

input

Proof

Use when

{"create": {"nonce": N}}

the contract was CREATE-deployed by the finalizing key at nonce N

plain EOA-deployed contracts

"firstStorageSlot"

storage slot 0 holds the finalizer address

contracts designed for it

"customStorageSlot"

the slot keccak256("HyperCore deployer") holds the finalizer address

deposit wallets, and any CREATE2 or factory-deployed contract (there is no CREATE2 proof variant, which is why the named-slot path exists)

Both actions are HyperCore L1 actions; nothing is signed on the EVM side. The finalize must be signed by whichever key the wallet's keccak256("HyperCore deployer") slot holds. The wallet records it at creation, and it can differ from the auction deployer.

Verify the link:

python
token = next(t for t in info.spot_meta()["tokens"] if t["index"] == TICKER_INDEX)
# → token["evmContract"]["address"] == the deposit wallet; deposits are now open

Bridging is now live, and holders use HyperCore bridging via deposit wallets. Every crossing lands on the token's system address (0x20…04d2 here): a deposit escrows the underlying in the wallet and HyperCore credits the recipient from the system address, and a withdrawal is a Core spot send to the system address that pays out the escrowed underlying on HyperEVM. A Core explorer's view of the system address is therefore the bridge's complete activity feed in both directions.

Step 6: List on a spot orderbook

Listing is optional and independently timed. Deposits and withdrawals work without it, but explorers show a pending token until it runs. Like everything else here, listing takes only permissionless deployer actions, with no review or approval.

Choose the quote asset

Spot orderbooks use USDC (token index 0) as the default quote asset, and a USDC quote needs no preparation. To list against a different quote asset, that asset must first be enabled as a quote token, through a spot-deploy action run for the quote token itself:

python
# Enable a token to serve as the quote side of pairs (skip for USDC).
# Listing your own two tokens against each other means running this on
# your quote token; eligibility conditions are defined by the protocol
# (see the official deploy docs).
exchange.spot_deploy_enable_quote_token(QUOTE_TOKEN_INDEX)

Hyperliquidity seeding targets USDC-quoted pairs (HIP-2), so list non-USDC pairs with nOrders = 0 (an empty orderbook) regardless of the genesis flag.

Register the pair, then list it

Two actions, in order. The second is what actually lists:

python
# Allocate the spot pair index (QUOTE_TOKEN_INDEX = 0 for USDC).
# The same action also deploys a pair between an existing base and an
# existing (quote-enabled) asset.
exchange.spot_deploy_register_spot(TICKER_INDEX, QUOTE_TOKEN_INDEX)

# The new spot index comes from your deploy state (user-keyed by deployer):
state = info.post("/info", {"type": "spotDeployState", "user": deployer.address})
# → state["states"] carries the ceremony progress; the allocated pair index
#   appears under its spots. The pair is NOT tradable yet.

# Finalize the listing. Required even with noHyperliquidity genesis, in which
# case n_orders MUST be 0 (empty orderbook) and order_sz is formal.
# start_px is the real initial listing price: set it realistically, not 1.
exchange.spot_deploy_register_hyperliquidity(SPOT_INDEX, START_PX, ORDER_SZ, 0, None)

Verify that the pair [TICKER_INDEX, QUOTE_TOKEN_INDEX] appears in spotMeta.universe and the pending status clears:

python
universe = info.spot_meta()["universe"]
assert any(p["tokens"] == [TICKER_INDEX, QUOTE_TOKEN_INDEX] for p in universe)

Common failures:

  • registerSpot alone leaves the pair unlisted; registerHyperliquidity finalizes it.
  • A non-USDC quote that was never quote-enabled blocks the pair. Run enableQuoteToken on the quote asset first.
  • With nOrders = 0, if orderSz = 0 is rejected, use 1. It is formal either way.
  • Never re-run genesis to clear the pending status.

Other spot-deploy actions

The deployer family has a few more members outside this guide's path. Payload shapes are shown for orientation; see the official deploy docs for semantics.

Action

Payload (inside {"type": "spotDeploy", …})

Purpose

setDeployerTradingFeeShare

{"token": i, "share": "…"} (share is a string)

set the deployer's cut of the pair's trading fees

enableFreezePrivilege / freezeUser / revokeFreezePrivilege

{"token": i} / {"token": i, "user": a, "freeze": b}

opt-in compliance controls; revocation is permanent

registerHyperliquidity with nOrders > 0

as in step 6, plus nSeededLevels

HIP-2 market-making seed for USDC-quoted pairs; requires deployer USDC (HIP-2)