Kinetiq

Elysium

Bridging an Elysium-native ERC-20 to HyperEVM through the mirror bridge, with zero bridge code on the token, a one-time two-transaction setup, and 1:1 enforced on-chain.

Last updated

A developer guide for bridging an Elysium-native ERC-20 to HyperEVM through the mirror bridge. It is the exact reverse of the canonical leg: lock on Elysium, mint a 1:1 mirror on HyperEVM.

The native token needs zero bridge code: no special interfaces, no mint authority granted to anything, no wrapping. Holders on Elysium always hold the original asset; the mirror on HyperEVM is a plain ERC-20 minted and burned only by the bridge. Once mirrored, a token can graduate to a HyperCore spot orderbook like any HyperEVM token. See Token bridging on Elysium for the full overview.

Elysium is pre-launch: the address constants in the samples (BRIDGE_FACTORY, TOKEN, …) are deliberately left undefined, so substitute the values published at launch. Details may be refined before mainnet.

1. Overview

The bridge uses two per-token contracts, both deployed by factories at derived addresses:

  • The escrow wallet (Elysium) holds the locked native token. It is deployed by ElysiumBridgeFactory.createL2Wallet(token), which derives the one mirror it will pair with from the token's live on-chain metadata.
  • The mirror (HyperEVM) is the 1:1 counterpart. It is deployed and registered by ElysiumMirrorFactory, keyed by the token plus its exact metadata (name, symbol, decimals).

Setup is one-time, permissionless, and takes one transaction per chain (either order). After that, locking on Elysium mints the mirror on HyperEVM (after the challenge period), and burning the mirror on HyperEVM releases the escrow on Elysium (typically within a minute).

At a glance

Value

ElysiumBridgeFactory (Elysium)

TBA at launch

ElysiumMirrorFactory (HyperEVM)

TBA at launch

Router (Elysium)

TBA at launch

Router (HyperEVM)

TBA at launch

Elysium chain ID

TBA at launch

Fee asset (both directions)

native HYPE via msg.value

Token assumptions. The token must implement IERC20Metadata (name(), symbol(), decimals()). The metadata is the pairing key, so tokens without it are not currently mirrorable. Its metadata should also be immutable: a token that rewrites its name or symbol creates a new pair per metadata snapshot, with the current-metadata pair canonical (section 3). Fee-on-transfer and rebasing tokens are not currently supported: the locked amount must arrive exactly and remain constant, because the mirror's supply has to match it 1:1.

2. One-time setup

Setup is permissionless and takes two transactions, which can be sent in either order.

Step 1: Deploy the escrow wallet (Elysium)

solidity
interface IElysiumBridgeFactory {
    // the token is the only input; the mirror is derived from its live metadata
    function createL2Wallet(address elysiumToken) external returns (address l2Wallet);
    // previews (no deployment needed)
    function expectedL1Mirror(address elysiumToken) external view returns (address);
    function predictL2Wallet(address elysiumToken) external view returns (address);
    function l2WalletFor(address elysiumToken) external view returns (address); // 0x0 if none
}
solidity
// BRIDGE_FACTORY: published at launch. TOKEN: the Elysium-native ERC-20.
address wallet = IElysiumBridgeFactory(BRIDGE_FACTORY).createL2Wallet(TOKEN);

The factory reads the token's name(), symbol(), and decimals() at call time, derives the one mirror address those values produce on HyperEVM, and deploys the wallet bound to that mirror alone. expectedL1Mirror(token) previews the pairing before deployment.

Step 2: Deploy and register the mirror (HyperEVM)

One payable transaction deploys the mirror at its deterministic address and registers it with the bridge on both chains:

solidity
interface IElysiumMirrorFactory {
    struct RegistrationParams {
        uint256 maxSubmissionCostForGateway;
        uint256 maxSubmissionCostForRouter;
        uint256 maxGasForGateway;
        uint256 maxGasForRouter;
        uint256 gasPriceBid;        // Elysium gas price bid
        uint256 valueForGateway;    // = maxSubmissionCostForGateway + maxGasForGateway * gasPriceBid
        uint256 valueForRouter;     // = maxSubmissionCostForRouter + maxGasForRouter * gasPriceBid
        address creditBackAddress;  // receives any excess, on Elysium
    }

    // msg.value = valueForGateway + valueForRouter
    function createAndRegisterL1Mirror(
        address elysiumToken, string calldata name, string calldata symbol, uint8 decimals,
        RegistrationParams calldata params
    ) external payable returns (address l1Mirror);

    // two-step alternative + previews
    function createL1Mirror(address elysiumToken, string calldata name, string calldata symbol, uint8 decimals)
        external returns (address l1Mirror);
    function registerL1Mirror(address mirror, RegistrationParams calldata params) external payable;
    function predictL1Mirror(address elysiumToken, string calldata name, string calldata symbol, uint8 decimals)
        external view returns (address);
}

Use the token's exact live metadata; any other tuple produces an inert variant (section 3). The registration sends two delivery messages to Elysium, funded by msg.value in native HYPE. Over-provisioning is safe (excess refunds to creditBackAddress on Elysium).

solidity
// MIRROR_FACTORY: published at launch. CREDIT_BACK: your address (excess refunds there,
// on Elysium). Use the token's exact live name/symbol/decimals.
uint256 gasPriceBid = 0.1 gwei;
uint256 maxGas = 300_000;
uint256 maxSubmissionCost = 4e14; // headroom; excess refunds on Elysium
uint256 valueEach = maxSubmissionCost + maxGas * gasPriceBid;

address mirror = IElysiumMirrorFactory(MIRROR_FACTORY).createAndRegisterL1Mirror{value: 2 * valueEach}(
    TOKEN, "My Token", "MTK", 18,
    IElysiumMirrorFactory.RegistrationParams({
        maxSubmissionCostForGateway: maxSubmissionCost,
        maxSubmissionCostForRouter: maxSubmissionCost,
        maxGasForGateway: maxGas,
        maxGasForRouter: maxGas,
        gasPriceBid: gasPriceBid,
        valueForGateway: valueEach,
        valueForRouter: valueEach,
        creditBackAddress: CREDIT_BACK
    })
);
The two steps are order-independent. Registration before wallet deployment is supported: bridging fails closed (TOKEN_NOT_DEPLOYED) until the wallet exists at its derived address. The registered binding is permanent. Repeating the registration for the same mirror re-sends the delivery messages (useful if one failed to execute).

Registration timing & verification

A successful registration transaction proves submission, not completion. In one HyperEVM transaction it does three things: writes the mirror's route into the HyperEVM router (immediate, same transaction), and sends two independent delivery messages to Elysium — one registering the wallet with the Elysium-side gateway, one writing the mirror's route into the Elysium router. Each delivery message normally auto-executes within a minute, but each can be individually delayed or dropped — underfunded messages stay claimable and anyone can re-execute them, and unredeemed messages can expire after an extended period, exactly like the transfer legs below.

Before moving any mirror supply, verify the route on both chains:

solidity
// HyperEVM: the route must resolve to the bridge's dedicated gateway (published at launch)
IGatewayRouter(HYPEREVM_ROUTER).getGateway(MIRROR);              // == MIRROR_BRIDGE_L1_GATEWAY
// Elysium: same check once the delivery messages have executed, plus the wallet round trip
IGatewayRouter(ELYSIUM_ROUTER).getGateway(MIRROR);               // == MIRROR_BRIDGE_L2_GATEWAY
IGatewayRouter(ELYSIUM_ROUTER).calculateL2TokenAddress(MIRROR);  // == the escrow wallet

Checking both chains matters because of how the routers resolve unrouted tokens: any token without an explicit route falls through to the chain's default (standard-bridge) gateway. Depositing a mirror in that state does not revert — the standard bridge escrows the mirror like any ordinary ERC-20 and mints an unrelated standard representation on Elysium. The funds are recoverable (withdraw that representation back through the standard bridge), but the result is a confusing look-alike token — precisely the kind the 1:1 rule says to classify by registry, never by existence. Until getGateway answers with the dedicated gateway on both chains, treat the pair as not yet open and hold mirror supply still.

The in-between states all fail safe. If the wallet-registration message hasn't executed on Elysium yet, deposits of the mirror bounce — the bridge automatically sends the funds back rather than crediting an unregistered wallet — and router-routed withdrawals on Elysium revert until the route lands. Recovery for a lost delivery message is one call: registerL1Mirror(mirror, params) again. A repeat with the same mirror re-sends both messages; the binding itself was already permanent, so repeats are always safe.

Choose creditBackAddress carefully: use an address you control on Elysium, and prefer a plain externally-owned account — the rollup stack delivers refunds addressed to contracts at an alias-transformed address, where they are easy to strand.

Common reverts: L2WalletExists (the pair is already deployed; read l2WalletFor), L1MirrorExists (same; read predictL1Mirror/l1MirrorFor), MsgValueMismatch (msg.value must equal valueForGateway + valueForRouter exactly), UnknownL1Mirror (registerL1Mirror on a mirror this factory didn't deploy), metadata call reverts (the token doesn't implement IERC20Metadata).

3. The 1:1 rule

Every Elysium token has exactly one live mirror, enforced on-chain rather than by convention:

  • Mirror identity is keyed by the token + its metadata tuple, so a wrong-metadata mirror is a different mirror at a different address.
  • The Elysium factory only ever deploys the wallet for the live-metadata pair, and the wallet's address commits to the factory's identity, so no other party can ever place code there.
  • Mirror supply is only minted against wallet escrow. Without the wallet, supply remains zero permanently. A wrong-metadata mirror can be created and registered, but it can never hold supply.

Resolve the canonical mirror programmatically:

solidity
// On Elysium, the factory is the oracle:
address canonicalMirror = IElysiumBridgeFactory(BRIDGE_FACTORY).expectedL1Mirror(TOKEN);
// -> the one canonical mirror for the token's current metadata

// On HyperEVM, check factory provenance + nonzero supply:
bool isCanonical = IElysiumMirrorFactory(MIRROR_FACTORY).elysiumTokenOf(mirror) != address(0)
    && IERC20(mirror).totalSupply() > 0;

4. Elysium → HyperEVM (lock, then mint)

Two transactions on Elysium: approve the wallet (the one bridge-specific step in this direction), then send through the router.

Step 1: Approve on the native token

solidity
// WALLET = IElysiumBridgeFactory(BRIDGE_FACTORY).l2WalletFor(TOKEN)
IERC20(TOKEN).approve(WALLET, 100e18); // 100 tokens at 18 dp

Step 2: Send on the Elysium router

The router keys the route on the mirror's address (the HyperEVM side of the pair):

solidity
interface IGatewayRouter {
    function outboundTransfer(address token, address to, uint256 amount,
        uint256 maxGas, uint256 gasPriceBid, bytes calldata data)
        external payable returns (bytes memory);
}
solidity
// ELYSIUM_ROUTER: published at launch. MIRROR = expectedL1Mirror(TOKEN).
// maxGas/gasPriceBid/data are unused in this direction: pass 0, 0, "".
IGatewayRouter(ELYSIUM_ROUTER).outboundTransfer(MIRROR, RECIPIENT, 100e18, 0, 0, "");

What happens: the wallet pulls exactly amount of the native token into escrow, and a message to HyperEVM is queued. After the challenge period it becomes executable, and executing it mints amount of the mirror to the recipient (the bridge UI surfaces claimable transfers; programmatic claiming executes against the bridge's outbox).

Common reverts: allowance/balance on the native token (approve the wallet), TOKEN_NOT_DEPLOYED (the wallet is not deployed yet; complete section 2, step 1), escrow shortfall for non-standard tokens (the locked amount must arrive exactly), EXTRA_DATA_DISABLED (pass empty data).

5. HyperEVM → Elysium (burn, then release)

One transaction on HyperEVM, and no approvals are required. The bridge burns the mirror directly from your balance, and the escrowed native token is released on Elysium, typically within a minute:

solidity
// HYPEREVM_ROUTER: published at launch.
uint256 maxGas = 300_000;
uint256 gasPriceBid = 0.1 gwei;
uint256 maxSubmissionCost = 4e14;
uint256 fee = maxSubmissionCost + maxGas * gasPriceBid;

IGatewayRouter(HYPEREVM_ROUTER).outboundTransfer{value: fee}(
    MIRROR, RECIPIENT, 100e18, maxGas, gasPriceBid, abi.encode(maxSubmissionCost, bytes(""))
);
Note the token argument is the mirror's address in both directions. The bridge keys every route on the HyperEVM side of the pair.

If the release leg fails to auto-execute on Elysium (for example, gasPriceBid was set below the live gas price), the message stays claimable and anyone can re-execute it. Funds are delayed, not lost. Re-execute promptly, as unredeemed messages can expire after an extended period.

Common reverts: mirror balance too low; msg.value below maxSubmissionCost + maxGas × gasPriceBid.

6. Graduating to HyperCore

The mirror on HyperEVM is an ordinary ERC-20, so it reaches a HyperCore spot orderbook the same way any HyperEVM token does: through a deposit wallet that HyperCore credits and debits against escrow. The issuer ceremony — HyperCore spot deployment (ticker auction, genesis, link, listing) — runs on the mirror unchanged, and the runtime flow in HyperCore bridging via deposit wallets then applies as-is.

7. Reference

ElysiumBridgeFactory (Elysium)

solidity
function createL2Wallet(address elysiumToken) external returns (address l2Wallet);
// views: all keyed by the token alone, following its live metadata
function expectedL1Mirror(address elysiumToken) external view returns (address);
function predictL2Wallet(address elysiumToken) external view returns (address);
function l2WalletFor(address elysiumToken) external view returns (address);

Errors: ZeroAddress, L2WalletExists.

ElysiumMirrorFactory (HyperEVM)

solidity
function createL1Mirror(address elysiumToken, string calldata name, string calldata symbol, uint8 decimals)
    external returns (address l1Mirror);
function registerL1Mirror(address mirror, RegistrationParams calldata params) external payable;
function createAndRegisterL1Mirror(address elysiumToken, string calldata name, string calldata symbol,
    uint8 decimals, RegistrationParams calldata params) external payable returns (address l1Mirror);
// views
function predictL1Mirror(address elysiumToken, string calldata name, string calldata symbol, uint8 decimals)
    external view returns (address);
function l1MirrorFor(address elysiumToken, string calldata name, string calldata symbol, uint8 decimals)
    external view returns (address);
function expectedL2Wallet(address mirror) external view returns (address);
function elysiumTokenOf(address mirror) external view returns (address);

Errors: ZeroAddress, L1MirrorExists, UnknownL1Mirror; on the mirror: MsgValueMismatch, NotRegistrar (registration runs through the factory only).

The mirror and the wallet

  • The mirror is a plain ERC-20 to everyone except the bridge, which alone mints and burns it. It has the same decimals as the native token, is uncapped at the contract level, and its supply always equals the escrow locked on Elysium.
  • The wallet is not an ERC-20, so don't add it to token lists. Users only ever touch it through the approve in section 4.

For the architecture deep dive (sequence diagrams, derivation mechanics, trust model), see Token bridging architecture.

Contract addresses and example transactions will be added at launch.