> For the complete documentation index, see [llms.txt](https://docs.lighter.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lighter.xyz/evm/interop/tokens/token-linking.md).

# Token Linking

{% hint style="info" %}
**Note**

This page is for governance and token admins who register and link assets.

Asset registration requires governance action. To get an asset registered on the devnet, reach out to the Lighter team.
{% endhint %}

Lighter supports assets whose native domain is Ethereum L1, Lighter EVM, or Lighter Core. Token linking connects tokens on their native domain to tokens on their non-native domains.

| Canonical domain | L1 representation | EVM representation | Core representation  |
| ---------------- | ----------------- | ------------------ | -------------------- |
| L1               | Canonical ERC-20  | Wrapped ERC-20     | Wrapped Core balance |
| EVM              | Wrapped ERC-20    | Canonical ERC-20   | Wrapped Core balance |
| Core             | None              | None               | Native Core asset    |

Token linking is not supported for Core-native assets, which have no representation on L1 or Lighter EVM. The rest of this page therefore covers L1-native and EVM-native ERC-20 assets.

ETH is linked across all three domains by default and requires no ERC-20 linking flow.

## Flow at a glance

**L1-native asset**

1. **Register.** [Governance](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/Governance.sol) registers the asset on L1, then on Core. See [L1-native assets](#l1-native-assets).
2. **Add the EVM wrapper (optional).** The Core token admin initiates wrapper setup from L1. EVM deploys or verifies the wrapper. See [Select the EVM wrapper](#enable-an-evm-wrapper-for-an-l1-native-asset).
   * A custom EVM wrapper token requires [proof of control](#proving-control-of-an-evm-token).

**EVM-native asset**

1. **Register.** [Governance](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/Governance.sol) registers the asset on L1, then on Core. See [EVM-native assets](#evm-native-assets).
2. **Prove control.** Set the canonical EVM token's admin slot to the Core token admin's address, then send its token-info report from EVM and execute it on L1 before linking. See [Proving control of an EVM token](#proving-control-of-an-evm-token).
3. **Link the canonical token.** The Core token admin initiates linking from L1. Wait for EVM and Core to record the token, then execute Core's confirmation on L1. See [Linking the canonical token](#linking-the-canonical-token).
4. **Add the L1 wrapper (optional).** The Core token admin initiates wrapper setup from L1, which deploys or verifies the L1 wrapper. See [Select the L1 wrapper](#enable-an-l1-wrapper-for-an-evm-native-asset).
   * A custom L1 wrapper token requires [proof of control](#proving-control-of-an-l1-token).

Linking makes an asset transferable. Trading it on Core also needs a market, which governance creates separately.

## Roles

There are two privileged roles that manage a token's linking lifecycle through the [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) contract on L1.

* **Governance** registers assets on Core and appoints their Core token admins. Governance also approves control of custom L1 wrapper tokens, see [Proving control of an L1 token](#proving-control-of-an-l1-token) for details.
* **The Core token admin** (`coreTokenAdmin`) is an L1 address that governance records for each asset at registration. It is the only address that can initiate token linking and wrapper setup for that asset. Governance can change the Core token admin after registration.

## Proving control of tokens

Before a `coreTokenAdmin` may associate a Core asset with an independently deployed token contract, it must prove control of that contract. This prevents a malicious Core token admin from claiming a token it does not own and thereby blocking the token's true owner from later linking it to the correct Core asset.

How control is proved depends on whether the token contract is on L1 or Lighter EVM.

### Proving control of an EVM token

On Lighter EVM, a token contract signals its controller by storing the controller's address in a protocol-defined storage slot.

The [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) can then send the slot value, together with the token's ERC-20 metadata, to [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol), which coordinates token-linking authorization and management on L1. When the token is proposed for linking, the reported controller is compared with the Core asset's `coreTokenAdmin`. The values must match before the link is authorized.

The detailed execution steps are described below.

**Step 1.** Set the token's admin slot to the asset's `coreTokenAdmin` address. The admin slot is:

```solidity
// keccak256("lighter.admin.slot")
bytes32 constant TOKEN_ADMIN_SLOT =
    0x4d2e8bd502eb517f6a9601bbafcec9c20dec0101dc1413f43ae2900ce65ed098;
```

Store the address in the low 20 bytes of the slot. The upper 12 bytes must be zero. Before the asset is registered, use the address governance will appoint as `coreTokenAdmin`.

**Step 2.** On Lighter EVM, call [`EvmInteropManager.sendTokenInfo(evmToken)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol), where `evmToken` is the address of the EVM token being linked. Anyone can make this call.

```solidity
function sendTokenInfo(address evmToken) external;
```

The token-info report includes the token's admin slot value at the end of the reporting EVM block and its ERC-20 name, symbol, and decimals. If `name()` or `symbol()` is missing, the report uses an empty string for that field. If `decimals()` is missing, it uses `18`. Each report is independent, and reports do not have to execute on L1 in EVM submission order. Executing a report overwrites the admin and metadata stored by an earlier report.

**Step 3.** [Execute](/evm/interop/message-passing/l1-and-evm.md#executing-the-call-on-l1) the resulting token-info report on L1. This records the token's admin address and metadata in [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol).

**Step 4.** Verify the recorded admin by calling [`L1InteropManager.evmApprovedTokenAdmin(evmToken)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1. It must return the asset's `coreTokenAdmin` address.

![EVM token proof of control, with a permissionless caller reporting through EvmInteropManager and EvmToL1Messenger, followed by separate execution on L1 through ZkLighter.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-e11824afe9692f987123ba64e312b7b8804bf98d%2Fevm-token-control-dark-87bccb52.svg?alt=media)

### Proving control of an L1 token

Unlike on Lighter EVM, the protocol cannot directly inspect an arbitrary L1 token's storage to verify who controls it. Governance therefore acts as the trusted attestor and records the address authorized to link each L1 token contract.

**Step 1.** Request that governance call [`L1InteropManager.setL1ApprovedTokenAdmin(l1Token, coreTokenAdmin)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1.

```solidity
function setL1ApprovedTokenAdmin(address token, address admin) external;
```

Only governance can make this call. Pass the L1 token address as `token` and the asset's `coreTokenAdmin` address as `admin`.

**Step 2.** Query [`L1InteropManager.l1ApprovedTokenAdmin(l1Token)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1. It must return the asset's `coreTokenAdmin` address.

![Governance calls L1InteropManager.setL1ApprovedTokenAdmin to record the approved admin for an L1 token.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-ac6240f704ac415f4458de39b1cd650ba47e5eaf%2Fl1-token-control-dark-488a848d.svg?alt=media)

## Registering on Core

Registering an asset on Core enables transfers between Core and the asset's canonical domain. For an EVM-native asset, the canonical EVM token must also be linked before those transfers become available.

[Governance](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/Governance.sol) registers an asset in two calls on [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol):

1. **Store the L1 configuration.** This call records:
   * `uint32 assetIndex`, the asset identifier.
   * `address coreTokenAdmin`, the Core token admin's address.
   * `uint128 tickSize`, how many of the token's smallest EVM units make up one [Core base unit](/evm/interop/tokens/token-transfers.md#amounts); see [Amounts](/evm/interop/tokens/token-transfers.md#amounts).
   * `uint56 extensionMultiplier`, Lighter's internal asset extension multiplier.
   * For an L1-native asset, the canonical L1 token address.
2. **Create the asset on Core.** This submits a priority request that creates the asset on Core with its Core-side parameters, such as symbol, minimum transfer and withdrawal amounts, and margin mode.

Core registration is a cross-domain request and can be retried if it fails. Linking cannot complete until Core has registered the asset.

### L1-native assets

[Governance](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/Governance.sol) calls `registerL1NativeAssetConfig`, then `registerL1NativeAssetOnCore`. Registration records the canonical L1 token address, so transfers between L1 and Core are available as soon as Core registration succeeds. No further linking is needed for that path.

![L1-native asset registration from governance through L1InteropManager, L1CoreMessaging, and ZkLighter to asynchronous processing by the Core sequencer.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-4908f73f9845fbcd8049413b3a47d5754c865011%2Fl1-native-registration-dark-e7f0019a.svg?alt=media)

### EVM-native assets

[Governance](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/Governance.sol) calls `registerEvmNativeAssetConfig`, then `registerEvmNativeAssetOnCore`. These calls create the asset on Core but do not associate it with an EVM token. To complete EVM-native registration, the Core token admin links the canonical token and waits for Core to record it.

![EVM-native asset registration from governance through the Core priority queue, creating a Core asset whose canonical EVM token still needs to be linked.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-4311c26a8c43f4d407a9c09c4104c6f2a0342add%2Fevm-native-registration-dark-1446bfdd.svg?alt=media)

#### Linking the canonical token

**Step 1.** Complete the governance calls for [EVM-native registration](#evm-native-assets).

**Step 2.** Ensure you have [proved control of the EVM token](#proving-control-of-an-evm-token), including delivery of its token-info report to L1.

**Step 3.** On L1, call [`L1InteropManager.linkEvmNativeToken`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) as `coreTokenAdmin`.

```solidity
function linkEvmNativeToken(
    uint32 assetIndex,
    address evmToken,
    uint256 gasLimit
) external payable;
```

| Type      | Parameter    | Description                                                                       |
| --------- | ------------ | --------------------------------------------------------------------------------- |
| `uint32`  | `assetIndex` | Index of the registered EVM-native Core asset.                                    |
| `address` | `evmToken`   | Address of the canonical EVM token whose approved admin matches `coreTokenAdmin`. |
| `uint256` | `gasLimit`   | Gas limit for the EVM token-linking transaction.                                  |

**Step 4.** Wait for EVM to record the token and notify Core. Query [`EvmInteropManager.getAssetConfig(assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) and confirm that its `evmToken` field matches the canonical EVM token address.

**Step 5.** Wait for Core to record the token and create its confirmation call to L1. Query Core's [EVM token address](/evm/interop/reading-core-from-evm.md#evm-token-address) and confirm that it equals `evmToken`.

**Step 6.** Execute the [Core-to-L1 confirmation](/evm/interop/message-passing/l1-and-core.md#executing-core-messages-on-l1). Confirm that [`L1InteropManager.evmNativeLinkedToken(assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) equals `evmToken` before adding an L1 wrapper.

![Canonical EVM token linking with separate asynchronous boundaries for L1 messaging and settlement and the EVM-to-Core write stream, followed by execution of the Core confirmation on L1.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-aa9d33b6269f624a33b318e2f5b05fd187207e87%2Fevm-canonical-token-link-dark-87b51961.svg?alt=media)

## Enabling the external wrapper — gateway selection

The final (optional) step in the token linking life cycle enables the wrapper on the external domain opposite the canonical token: EVM for an L1-native asset, or L1 for an EVM-native asset. This process is called gateway selection.

Gateway selection chooses the external wrapper token and two gateway contracts, one on the canonical domain and one on the wrapped-token domain. The contract on the canonical domain takes custody of the underlying tokens, while the contract on the other domain manages the selected wrapper. For example, an EVM-native asset uses an escrow contract on Lighter EVM, called the [**escrow gateway**](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interfaces/IEscrowGateway.sol), and a minting contract on L1, called the [**mint gateway**](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interfaces/IMintGateway.sol). These counterpart contracts form a **gateway pair** and are selected together.

[`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) and [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) do not manage the wrapper token directly. They operate it through the selected gateways using standard interfaces. The mint gateway holds whatever permissions the wrapper requires for minting and burning, rather than granting those permissions to `L1InteropManager` or `EvmInteropManager`.

The gateways may also use the cross-domain messaging system to support transfers directly between L1 and Lighter EVM, independently of Core. Notably, the gateways (and therefore that bridge) can exist before the token is registered on Core and before `L1InteropManager` and/or `EvmInteropManager` become aware of it. As long as the gateways adhere to the appropriate interfaces, gateway selection can later connect the existing custody and wrapper system to the Core asset.

The [default gateways](#default-gateways) provide an example of a gateway pair that enables this direct L1–EVM bridge.

Transfers between Core and the canonical domain do not require gateways. They become available once the [registration process](#registering-on-core) is complete.

After registration, but before gateway selection, deposits from the canonical domain into Core are escrowed by [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) for L1-native assets or [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) for EVM-native assets. An independently operating L1–EVM bridge holds its escrow in its own gateway.

During gateway selection, any canonical tokens held by [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) or [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) move to the selected escrow gateway, delegating custody of the tokens to the gateways.

### Gateway requirements

Every gateway selected for Core integration must implement the interfaces below so that [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) and [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) can interact with it. The Core token admin is responsible for selecting compatible contracts—it may use the protocol's [default gateways](#default-gateways) or implement and deploy its own pair.

A gateway pair must preserve the asset's backing invariant—the canonical tokens held by the escrow gateway must cover the asset's combined representations on Core and the wrapped-token domain.

The interfaces define only the behavior required for Core integration. Gateways may provide additional functionality, such as direct transfers between L1 and Lighter EVM. Because the contracts exist independently of Core, a gateway pair may be deployed before or after the Core asset is created. Gateway selection can later connect an existing pair to the Core asset.

#### Gateway interfaces

The gateway on the canonical domain implements [`IEscrowGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interfaces/IEscrowGateway.sol):

```solidity
interface IEscrowGateway {
    function externalGateway() external view returns (address);
    function unlock(address localToken, address to, uint256 amount) external;
}
```

* `externalGateway()` returns the address of the paired mint gateway on the other domain: EVM for an L1 escrow gateway, or L1 for an EVM escrow gateway.
* `unlock(localToken, to, amount)` transfers `amount` of the canonical token at `localToken` from the gateway's escrow to the recipient `to`.

The gateway on the wrapped-token domain implements [`IMintGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interfaces/IMintGateway.sol):

```solidity
interface IMintGateway {
    function externalGateway() external view returns (address);
    function checkOrDeployToken(
        bytes32 salt,
        string calldata name,
        string calldata symbol,
        uint8 decimals
    ) external;
    function mint(address localToken, address to, uint256 amount, bytes calldata data) external;
    function managerBurn(address localToken, uint256 amount) external;
}
```

* `externalGateway()` returns the address of the paired escrow gateway on the other domain: EVM for an L1 mint gateway, or L1 for an EVM mint gateway.
* `checkOrDeployToken(...)` must ensure the selected wrapper exists. The supplied `salt` is `bytes32(uint256(uint160(canonicalToken)))`. [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) or [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) calls this function during gateway selection, after the canonical token has been successfully linked to the Core asset. The manager derives the `salt` from the canonical token in the authenticated asset configuration, so the gateway can treat it as a protocol-authenticated identifier within its canonical-domain namespace and safely use it for deterministic CREATE2 or CREATE3 wrapper deployment, if desired.
* `mint(...)` mints wrapped tokens to the recipient.
* `managerBurn(...)` is called by [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1 or [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) on Lighter EVM after that contract transfers the wrapped tokens directly from the user to the mint gateway. The function burns those tokens from the gateway's balance.

The gateway must allow `managerBurn(...)` calls from [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1 or [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) on Lighter EVM. Whether to reject calls from other addresses is up to the gateway designer.

For `unlock(...)` and `mint(...)`, allow calls from [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) on L1 and [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) on Lighter EVM.

### External wrapper requirements

Before selecting a custom wrapper, establish that the `coreTokenAdmin` controls it. For an EVM wrapper, set the protocol-defined admin slot and deliver it to L1 by calling [`sendTokenInfo(...)`](#proving-control-of-an-evm-token). For an L1 wrapper, [governance records the approved admin](#proving-control-of-an-l1-token).

The protocol also supplies the canonical token's `name()`, `symbol()`, and `decimals()` to the mint gateway through `checkOrDeployToken(...)`. For an EVM-native token, these values are recorded by `sendTokenInfo(...)`. For an L1-native token, [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) reads them during gateway selection. A missing `name()` or `symbol()` produces an empty string, and a missing `decimals()` produces `18`. The wrapper should use the same decimals as the canonical token because gateway operations pass token amounts between domains without conversion.

### Default gateways

The default gateways are a protocol-provided special case of gateway pairs. They implement the same gateway interfaces and satisfy the [gateway requirements](#gateway-requirements). Unlike a custom pair, their wrapper deployment behavior is fixed by the protocol.

Each default mint gateway uses CREATE2 to deploy a wrapper backed by a protocol-configured [`LighterWrappedERC20V1`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/token/LighterWrappedERC20V1.sol) implementation. The wrapper is initialized with the canonical token's `name()`, `symbol()`, and `decimals()` without adding a prefix or suffix. A missing `name()` or `symbol()` produces an empty string, and a missing `decimals()` produces `18`.

If the wrapper does not yet exist, the transaction that deploys it must have enough gas for both `CREATE2` and `initialize(...)`, in addition to the rest of the operation. For deployment through an EVM priority request, include these costs in its `gasLimit`: a delivery that deploys a default EVM wrapper uses about 450,000 gas.

Default wrappers are upgradeable, and all wrappers deployed by a given mint gateway share the same implementation.

The deployed protocol contracts recognize the default [L1 gateway addresses](/evm/chain-info.md#protocol-smart-contracts) and [EVM gateway addresses](/evm/chain-info.md#predeploys). Passing those addresses during gateway selection selects this known deployment path, allowing the protocol to derive and verify the wrapper address instead of requiring proof that the Core token admin controls it. The wrapper can therefore bypass the proof-of-control protocol.

The protocol provides the following default gateway implementations:

| Asset      | L1 gateway                                                                                                                                           | EVM gateway                                                                                                                                             |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| L1-native  | [`DefaultL1EscrowGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/l1/DefaultL1EscrowGateway.sol) | [`DefaultEvmMintGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/l1/DefaultEvmMintGateway.sol)      |
| EVM-native | [`DefaultL1MintGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/evm/DefaultL1MintGateway.sol)    | [`DefaultEvmEscrowGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/evm/DefaultEvmEscrowGateway.sol) |

The default gateway pairs also support direct L1–EVM bridging. If you want users to bridge between L1 and EVM before the Core asset exists, and you intend to use the default gateways for its eventual Core integration, you can direct users to the default gateway pair immediately. The Core asset can be registered and linked to the same pair later.

For L1-to-EVM transfers, the default gateways use [`L1ReplayableMessenger`](/evm/interop/message-passing/l1-and-evm.md#l1replayablemessenger). Failed delivery can be retried, but each message can execute successfully only once.

### Selecting the gateway pair

#### Enable an EVM wrapper for an L1-native asset

During gateway selection, [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) reads the L1 token's metadata and passes it to the selected EVM mint gateway through `checkOrDeployToken(...)`. Missing metadata fields use the fallbacks described above. The protocol treats this metadata as stable and does not synchronize changes to a wrapper that already exists, including one deployed before gateway selection.

**Prerequisites.** Complete [L1-native registration](#l1-native-assets). If using a custom EVM wrapper, also complete its [proof of control](#proving-control-of-an-evm-token).

**Step 1.** Choose the gateway pair and `expectedEvmWrapperToken`. With defaults, read [`EvmMintGateway.computeTokenAddress(l1Token)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/l1/DefaultEvmMintGateway.sol) on EVM. With custom gateways, pass the appropriate wrapper token address.

**Step 2.** On L1, call [`L1InteropManager.selectL1NativeGatewayPair`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) as `coreTokenAdmin` with the selected EVM mint and L1 escrow gateway addresses. For defaults, use [`EvmMintGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/l1/DefaultEvmMintGateway.sol) and [`L1EscrowGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/l1/DefaultL1EscrowGateway.sol) at the addresses listed in [Chain Info](/evm/chain-info.md).

```solidity
function selectL1NativeGatewayPair(
    uint32 assetIndex,
    address evmGateway,
    address l1Gateway,
    address expectedEvmWrapperToken,
    uint256 gasLimit
) external payable;
```

| Type      | Parameter                 | Description                                                                                               |
| --------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `uint32`  | `assetIndex`              | Index of the registered L1-native Core asset.                                                             |
| `address` | `evmGateway`              | Address of the EVM mint gateway.                                                                          |
| `address` | `l1Gateway`               | Address of the L1 escrow gateway.                                                                         |
| `address` | `expectedEvmWrapperToken` | Expected EVM wrapper address; must match the default mint gateway's computed address when using defaults. |
| `uint256` | `gasLimit`                | Gas limit for the EVM gateway-selection transaction.                                                      |

**Step 3.** Wait for EVM to deploy or verify the wrapper. Query [`EvmInteropManager.getAssetConfig(assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) and confirm that its `evmToken` field equals `expectedEvmWrapperToken`.

**Step 4.** Execute the [acknowledgment on L1](/evm/interop/message-passing/l1-and-evm.md#executing-the-call-on-l1). This finalizes the L1 gateway, moves any existing escrow into it, and notifies Core of the EVM wrapper.

**Step 5.** Wait for Core to record the wrapper. Query Core's [EVM token address](/evm/interop/reading-core-from-evm.md#evm-token-address) and confirm that it equals `expectedEvmWrapperToken`.

![L1-native gateway selection, with externalGateway() counterpart checks on both domains, EVM wrapper deployment or verification, an acknowledgment executed on L1, transfer of existing escrow to the L1 escrow gateway, and a Core priority request to record the wrapper.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-3e2087bef0e9fc0f756f91b77770323565de2c8f%2Fl1-native-gateway-selection-dark-e04728fe.svg?alt=media)

#### Enable an L1 wrapper for an EVM-native asset

During [EVM-native registration](#evm-native-assets), `sendTokenInfo(...)` reports the canonical EVM token's `name()`, `symbol()`, and `decimals()` to L1. Each executed report overwrites metadata previously stored by [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol). Gateway selection passes the stored snapshot to the selected L1 mint gateway through `checkOrDeployToken(...)`. Later reports or metadata changes do not update an already deployed wrapper.

**Prerequisites.** Complete [EVM-native registration](#evm-native-assets). If using a custom L1 wrapper, also complete its [governance approval](#proving-control-of-an-l1-token).

**Step 1.** Choose the gateway pair and `expectedL1WrapperToken`. With defaults, read [`L1MintGateway.computeTokenAddress(evmToken)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/evm/DefaultL1MintGateway.sol) on L1. With custom gateways, pass the appropriate wrapper token address.

**Step 2.** On L1, call [`L1InteropManager.selectEvmNativeGatewayPair`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) as `coreTokenAdmin` with the selected EVM escrow and L1 mint gateway addresses. For defaults, use [`EvmEscrowGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/evm/DefaultEvmEscrowGateway.sol) and [`L1MintGateway`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/gateways/evm/DefaultL1MintGateway.sol) at the addresses listed in [Chain Info](/evm/chain-info.md). This call deploys or verifies the L1 wrapper using the previously reported token metadata.

```solidity
function selectEvmNativeGatewayPair(
    uint32 assetIndex,
    address evmGateway,
    address l1Gateway,
    address expectedL1WrapperToken,
    uint256 gasLimit
) external payable;
```

| Type      | Parameter                | Description                                                                                              |
| --------- | ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `uint32`  | `assetIndex`             | Index of the EVM-native Core asset. Its canonical token link must be finalized on L1.                    |
| `address` | `evmGateway`             | Address of the EVM escrow gateway.                                                                       |
| `address` | `l1Gateway`              | Address of the L1 mint gateway.                                                                          |
| `address` | `expectedL1WrapperToken` | Expected L1 wrapper address; must match the default mint gateway's computed address when using defaults. |
| `uint256` | `gasLimit`               | Gas limit for the EVM gateway-selection transaction.                                                     |

**Step 3.** Wait for EVM to select its escrow gateway and move any existing escrow into it. Query [`EvmInteropManager.getAssetConfig(assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) and confirm that its `evmGateway` field matches the selected EVM escrow gateway address.

**Step 4.** Execute the [acknowledgment on L1](/evm/interop/message-passing/l1-and-evm.md#executing-the-call-on-l1). This records the L1 gateway and wrapper and notifies Core of the L1 wrapper.

**Step 5.** Wait for Core to record the L1 wrapper. Query Core's [L1 token address](/evm/interop/reading-core-from-evm.md#l1-token-address) and confirm that it equals `expectedL1WrapperToken`.

![EVM-native gateway selection, with externalGateway() counterpart checks on both domains, L1 wrapper deployment or verification, transfer of existing escrow to the EVM escrow gateway, an acknowledgment executed on L1, and a Core priority request to record the wrapper.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-90fc349e96ae53dedb0a2faca362ae1e52de8ef6%2Fevm-native-gateway-selection-dark-24748d47.svg?alt=media)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.lighter.xyz/evm/interop/tokens/token-linking.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
