> 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-transfers/evm-and-core.md).

# EVM and Core

## EVM to Core

Any EVM address can send funds to a Core master account by calling [`EvmInteropManager.sendToCore`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol). The [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) takes the funds from the caller on EVM and submits a request for Core to credit the recipient account. Transfers to Core subaccounts are not supported.

The recipient master account is identified by its controlling address, which is either a Lighter EVM address or an Ethereum L1 address; see [Core accounts](/evm/interop/accounts.md#core-accounts). An EVM-controlled account is created on the first transfer if it does not exist yet. An L1-controlled account must already exist, otherwise the call reverts.

The first transfer to an EVM-controlled recipient requires an additional 146,000 gas to allocate account state on Core.

### Submit a transfer

```solidity
function sendToCore(
    uint32 assetIndex,
    address to,
    bool isL1Account,
    uint256 amount,
    RouteType routeType
) external payable returns (uint64 reqId);
```

| Type                                                         | Parameter     | Description                                                                                                                 |
| ------------------------------------------------------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `uint32`                                                     | `assetIndex`  | Asset identifier.                                                                                                           |
| `address`                                                    | `to`          | Controlling address of the recipient Core master account.                                                                   |
| `bool`                                                       | `isL1Account` | `true` for an L1-controlled Core account; `false` for an EVM-controlled Core account.                                       |
| `uint256`                                                    | `amount`      | Amount in the token's smallest units on EVM, such as wei for ETH. Must be nonzero and a multiple of the asset's `tickSize`. |
| [`RouteType`](/evm/interop/tokens/token-transfers.md#routes) | `routeType`   | Destination balance: `Perps = 0` or `Spot = 1`.                                                                             |

1. For an ERC-20, approve [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) to spend `amount` of the configured EVM token. The contract pulls tokens from the caller.
2. Call [`EvmInteropManager.sendToCore`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol). For ETH, `msg.value` must equal `amount` exactly. For an ERC-20, `msg.value` must be zero.
3. Keep the returned `reqId` to [check the transfer's status](#check-transfer-status) once Core has processed it.

In examples below, `coreOwner` is the address controlling the recipient's Core master account. Set `isL1Account` to `true` for an L1-controlled account or `false` for an EVM-controlled account. Choose `routeType` as `RouteType.Perps` or `RouteType.Spot` to select the balance to credit.

After approving the manager, send ERC-20 tokens to the selected Core balance:

```solidity
uint64 reqId = EvmInteropManager.sendToCore(
    assetIndex,
    coreOwner,
    isL1Account,
    amount,
    routeType
);
```

For an ETH transfer to the same Core account and balance:

```solidity
uint64 reqId = EvmInteropManager.sendToCore{value: amount}(
    1,      // assetIndex: ETH
    coreOwner,
    isL1Account,
    amount,
    routeType
);
```

For tokens that support EIP-2612, use `sendToCoreWithPermit` to authorize spending and submit the transfer in one call. Pass the same parameters as `sendToCore`, followed by the permit's `deadline`, `v`, `r`, and `s`. The permit authorizes the manager to spend `amount` from the caller.

If the EVM call reverts, no assets move and no request is queued. A successful EVM transaction confirms that the request was submitted, but does not confirm how much Core credited.

### Check transfer status

`sendToCore` returns a `uint64 reqId` that identifies the request on Core. To check its outcome, look up `reqId` with the [transaction-status read](/evm/interop/reading-core-from-evm.md#transaction-status). Once Core has processed the request, the read returns `status == 1`. The response's `acceptedAmount` gives the amount credited in [Core base units](/evm/interop/tokens/token-transfers.md#amounts), which may be less than the requested amount or zero. See [Send to Core](/evm/interop/message-passing/evm-and-core.md#send-to-core) for response decoding. Any unaccepted amount is refunded; see [Send to Core Refunds](#send-to-core-refunds).

### Send to Core Refunds

For each `sendToCore` deposit, the Core sequencer determines how much to credit on Core. The [Send to Core response](/evm/interop/message-passing/evm-and-core.md#send-to-core) reports this as `acceptedAmount`, expressed in [Core base units](/evm/interop/tokens/token-transfers.md#amounts), ranging from zero to `amount / tickSize`.

The sequencer currently enforces these rules:

* It credits nothing when the asset is not registered on Core.
* It credits nothing when the route is `Perps` and the asset is not enabled as margin for the recipient account; see [Routes](/evm/interop/tokens/token-transfers.md#routes).
* It credits less than the full amount, possibly nothing, when the deposit would take the account's balance of the asset past Core's maximum.

If the sequencer accepts less than the full `amount`, or none of it, Core refunds the difference. The refund amount, in units of the token's smallest denomination on EVM, is `amount - acceptedAmount * tickSize`. Refunds belong to the recipient `to`, even when a different address called `sendToCore`.

Core schedules the refund as a separate EVM transaction, so confirm its delivery separately from Core acceptance. Where it is delivered depends on the recipient's domain:

* **EVM-controlled recipient:** the refund goes to `to` on EVM. An ERC-20 refund passes through [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol). An ETH refund is a plain value transfer to `to`.
* **L1-controlled recipient:** the refund becomes a claim owned by the L1 address `to` in [`EvmL1RefundClaimer`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol). The owner must claim it from L1, as described below.

#### Refund delivery transaction

The [Core system address](/evm/chain-info.md#system-addresses) delivers the refund in a single EVM transaction with a fixed gas limit. The call and its gas limit depend on the recipient's domain and the asset:

| Recipient      | Asset  | EVM delivery                                                                                                                                                                                                                                                                                                                                                    | Transaction gas limit |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| EVM-controlled | ETH    | Sent directly to `to` with empty calldata.                                                                                                                                                                                                                                                                                                                      | 50,000                |
| EVM-controlled | ERC-20 | [`EvmInteropManager.receiveFromCore`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) releases or mints the tokens to `to`.                                                                                                                                                                    | 350,000               |
| L1-controlled  | ETH    | [`EvmL1RefundClaimer.receiveL1AccountRefund`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) receives the value and records the claim.                                                                                                                                                     | 100,000               |
| L1-controlled  | ERC-20 | [`EvmInteropManager.receiveL1AccountRefund`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) releases or mints the tokens to [`EvmL1RefundClaimer`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) and records the claim. | 380,000               |

The ERC-20 gas limits cover the full refund transaction, including manager, gateway, and token execution. An EVM-controlled refund uses the same delivery mechanism as a Core-to-EVM transfer; see [Delivery on EVM](#delivery-on-evm). A contract at `to` that receives an ETH refund must accept an ETH transfer with empty calldata within the 50,000 gas limit.

{% hint style="warning" %}
**Warning**

If the refund transaction reverts, for example because delivery does not fit within its gas limit, it is not retried. The unaccepted amount is neither credited on Core nor delivered on EVM, so the funds are lost.
{% endhint %}

#### Claim an L1-controlled recipient's refund

A refund for an L1-controlled recipient is not paid out automatically. [`EvmL1RefundClaimer`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol), a [predeploy](/evm/chain-info.md#predeploys) on EVM, holds the refund until the owning L1 address claims it.

To claim the refund, the owning L1 address must send a message through [`L1ReplayableMessenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/replayable/L1ReplayableMessenger.sol) on L1 that calls [`EvmL1RefundClaimer.claimTo`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) on EVM. The claim pays the full accumulated refund balance to an EVM address chosen by the owner.

```solidity
function claimTo(uint32 assetIndex, uint64 claimNonce, address recipient) external;
```

`claimTo` releases the claim for `assetIndex` owned by the L1 address that sent the message and pays it to `recipient`, which must be a nonzero address.

Claiming a refund has three steps.

**Step 1.** On EVM, read [`EvmL1RefundClaimer.claims(l1Address, assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) for the claim amount in [Core base units](/evm/interop/tokens/token-transfers.md#amounts) and [`EvmL1RefundClaimer.claimNonces(l1Address, assetIndex)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) for the current claim nonce. `l1Address` is the L1 address that owns the claim, which is the `to` passed to `sendToCore`.

**Step 2.** From `l1Address`, call [`L1ReplayableMessenger.sendMessage`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/replayable/L1ReplayableMessenger.sol) to deliver a `claimTo` call to [`EvmL1RefundClaimer`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) on EVM. Use the following parameters:

| Parameter       | Value                                                                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `target`        | The [`EvmL1RefundClaimer`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmL1RefundClaimer.sol) address. |
| `message`       | ABI-encoded `claimTo(assetIndex, claimNonce, recipient)`, where `recipient` is the nonzero EVM address that receives the funds.                         |
| `gasLimit`      | Gas limit for the claim transaction on EVM.                                                                                                             |
| `value`         | `0`.                                                                                                                                                    |
| `refundAddress` | L1 address that receives any excess `msg.value`.                                                                                                        |
| `msg.value`     | At least the bridge's `computePriorityFee(gasLimit)`.                                                                                                   |

**Step 3.** Confirm execution on EVM through the claimer's `L1AccountRefundClaimed` event. A successful claim transfers the full accumulated refund balance, clears that balance, and increments the nonce. Partial claims are not supported.

## Core to EVM

Core debits the selected account balance and schedules delivery on EVM. Initiation can come from a signed Core transaction or a call from the controller's native domain.

### Supported transactions

| Initiation method                                 | How to initiate it                                                                                                                                                                      |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Signed Core transaction](#send-to-evm-from-core) | Submit a signed Core transaction.                                                                                                                                                       |
| [L1 contract call](#l1-initiated-send-to-evm)     | Call [`L1CoreMessaging.sendToEvm`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/L1CoreMessaging.sol) from the controlling L1 address.    |
| [EVM contract call](#evm-initiated-send-to-evm)   | Call [`EvmInteropManager.sendToEvm`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) from the controlling EVM address. |

All three paths debit a Core account and use the same EVM delivery mechanism.

#### Send to EVM (from Core)

Submit a signed Core transaction with `txType = 128`. The transaction debits `baseAmount` from the account's selected balance, perps or spot, and charges `usdcFee` from the USDC balance on the same route.

See [Sign a Send to EVM transaction](/evm/signing-core-transactions.md#send-to-evm) for a JavaScript example using the WASM signer.

**Parameters**

| Field          | Description                                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `chainId`      | [Core chain ID](/evm/chain-info.md#chain-metadata), not the EVM chain ID.                                                                   |
| `txType`       | `128` for Send to EVM.                                                                                                                      |
| `nonce`        | Core account nonce.                                                                                                                         |
| `expiredAt`    | Core transaction expiration time as a Unix timestamp in milliseconds.                                                                       |
| `accountIndex` | Core account whose balance is debited.                                                                                                      |
| `apiKeyIndex`  | Index of the registered Core API key used to sign the transaction.                                                                          |
| `assetIndex`   | Asset to transfer; `1` for ETH.                                                                                                             |
| `evmRecipient` | Recipient on Lighter EVM; must not be the zero address.                                                                                     |
| `routeType`    | Source balance: `0` for perps; `1` for spot.                                                                                                |
| `baseAmount`   | Amount in [Core base units](/evm/interop/tokens/token-transfers.md#amounts): `1 <= baseAmount <= 2^60 - 1`.                                 |
| `usdcFee`      | Authorized fee in [Core USDC base units](/evm/interop/tokens/token-transfers.md#amounts) (10^6 units per USDC): `0 <= usdcFee <= 2^60 - 1`. |

Currently, always use `0` for `usdcFee` on the devnet. This is subject to change.

One [Core base unit](/evm/interop/tokens/token-transfers.md#amounts) equals `tickSize` of the token's smallest units on EVM. The recipient therefore receives `baseAmount * tickSize` in those units. For ETH, one [Core base unit](/evm/interop/tokens/token-transfers.md#amounts) equals `10^10` wei. See [Amounts](/evm/interop/tokens/token-transfers.md#amounts) for conversion examples and how to read an asset's `tickSize`.

This transaction does not accept L2 transaction attributes (the optional key-value attributes supported by some Core transactions).

**Authorization**

Every signed Core transfer requires a Schnorr signature from the registered Core API key. It also requires an ECDSA signature from the account's controlling address unless the recipient's EVM-controlled Core account index equals `accountIndex`:

| Condition                                                                                                                           | Required signatures                                                                      |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| The recipient's [EVM-controlled Core account index](/evm/interop/reading-core-from-evm.md#evm-account-index) equals `accountIndex`. | API-key Schnorr signature.                                                               |
| The recipient has no EVM-controlled Core account, or its account index differs from `accountIndex`.                                 | API-key Schnorr signature and an ECDSA signature from the account's controlling address. |

Sending from a subaccount to its master account's EVM address still requires the additional ECDSA signature.

The Schnorr signature is performed over the Core transaction hash. When an ECDSA signature is required, the master address signs the Send to EVM authorization message using EIP-191 personal-sign. Core verifies this signature against the account's stored `l1Address` or `evmAddress`, whichever is set.

For non-self sends, if the Core account's controlling address is a smart contract, the contract itself must initiate the transfer. See [EVM-initiated send to EVM](#evm-initiated-send-to-evm) and [L1-initiated send to EVM](#l1-initiated-send-to-evm).

**Balance and fee requirements**

* The selected route must have at least `baseAmount` of the transferred asset available. For the perps route, the asset must be enabled as margin in the account's per-asset configuration; USDC is always enabled.
* The same route must also have enough USDC to pay `usdcFee`. For a USDC transfer, the available USDC balance must cover `baseAmount + usdcFee`.
* The sequencer sets the fee required to accept the transaction, and `usdcFee` must match it. The sequencer rejects a transaction whose `usdcFee` does not match the fee it requires.

#### L1-initiated send to EVM

The address controlling an [L1-controlled Core account](/evm/interop/accounts.md#l1-controlled-accounts) can initiate a transfer by calling [`L1CoreMessaging.sendToEvm`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/L1CoreMessaging.sol) on Ethereum L1. See [Chain Info](/evm/chain-info.md#protocol-smart-contracts) for the contract address.

**Call**

```solidity
function sendToEvm(
    uint48 accountIndex,
    uint32 assetIndex,
    address evmRecipient,
    RouteType routeType,
    uint64 baseAmount
) external;
```

**Parameters**

| Type                                                         | Parameter      | Description                                                                                                 |
| ------------------------------------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `uint48`                                                     | `accountIndex` | Core account to debit: the caller's master account or one of its subaccounts.                               |
| `uint32`                                                     | `assetIndex`   | Asset to transfer; `1` for ETH.                                                                             |
| `address`                                                    | `evmRecipient` | Recipient on Lighter EVM; must not be the zero address.                                                     |
| [`RouteType`](/evm/interop/tokens/token-transfers.md#routes) | `routeType`    | Source balance: `Perps = 0` or `Spot = 1`.                                                                  |
| `uint64`                                                     | `baseAmount`   | Amount in [Core base units](/evm/interop/tokens/token-transfers.md#amounts): `1 <= baseAmount <= 2^60 - 1`. |

The caller authorizes the transfer through `msg.sender`. This method charges no Core USDC fee. The transferred asset must be supported, and the selected route must have sufficient funds. For the perps route, the asset must be enabled as margin in the account's per-asset configuration; USDC is always enabled.

Core processes the request asynchronously after the L1 call, then schedules delivery on EVM.

#### EVM-initiated send to EVM

The address controlling an [EVM-controlled Core account](/evm/interop/accounts.md#evm-controlled-accounts) can initiate a transfer by calling [`EvmInteropManager.sendToEvm`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) on Lighter EVM. The `accountIndex` must identify that address's master account or one of its subaccounts. The asset must exist on both Core and EVM. Core charges no USDC fee for this request.

```solidity
function sendToEvm(
    uint48 accountIndex,
    uint32 assetIndex,
    address evmRecipient,
    RouteType routeType,
    uint64 baseAmount
) external returns (uint64 reqId);
```

| Type                                                         | Parameter      | Description                                                                              |
| ------------------------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------- |
| `uint48`                                                     | `accountIndex` | Core account to debit.                                                                   |
| `uint32`                                                     | `assetIndex`   | Asset identifier.                                                                        |
| `address`                                                    | `evmRecipient` | Recipient address on EVM.                                                                |
| [`RouteType`](/evm/interop/tokens/token-transfers.md#routes) | `routeType`    | Source balance: `Perps = 0` or `Spot = 1`.                                               |
| `uint64`                                                     | `baseAmount`   | Amount to transfer in [Core base units](/evm/interop/tokens/token-transfers.md#amounts). |

Use the returned `reqId` to check the [Core response](/evm/interop/message-passing/evm-and-core.md#evm-initiated-send-to-evm). A successful Core status confirms that the transfer was scheduled, not that it was delivered on EVM.

### Delivery on EVM

On successful Core execution, the balance is debited and the transfer is scheduled for EVM execution. The recipient receives assets as follows:

| Asset             | EVM delivery                                                 | Transaction gas limit |
| ----------------- | ------------------------------------------------------------ | --------------------- |
| ETH               | Sent directly to `evmRecipient` with empty calldata.         | 50,000                |
| EVM-native ERC-20 | Canonical tokens are released from escrow to `evmRecipient`. | 350,000               |
| L1-native ERC-20  | The linked EVM wrapper is minted to `evmRecipient`.          | 350,000               |

A contract receiving ETH must accept an ETH transfer with empty calldata within the transaction's 50,000 gas limit.

The ERC-20 gas limit covers the full token-delivery transaction, including manager, gateway, and token execution.

EVM delivery transactions incur no fees on EVM. The `usdcFee` charged for a signed Core transaction is separate.

Check the EVM delivery transaction's receipt to confirm that delivery succeeded.

{% hint style="warning" %}
**Warning**

If delivery on EVM reverts, the transfer is not retried and the amount debited on Core is not restored.
{% endhint %}

#### Finding the delivery transaction

Core does not expose which delivery transaction belongs to which request, so find it by its contents. Every delivery is a `0x7E` transaction from the [Core system address](/evm/chain-info.md#system-addresses), and its fields carry the asset, recipient, and amount even if it reverts:

| Asset  | Delivery transaction                                                                                                                                                                                                                                                                                                                                                                               |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ETH    | `to` is the recipient, `value` is `baseAmount * tickSize`, and the input is empty. No event is emitted.                                                                                                                                                                                                                                                                                            |
| ERC-20 | `to` is [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol), and the input is `receiveFromCore(uint32 assetIndex, address to, uint128 baseAmount)`. On success, it emits `ReceivedFromCore(uint32 indexed assetIndex, address indexed to, uint256 amount)`, with `amount` in the token's smallest units on EVM. |

Deliveries appear on EVM in the order Core creates them, once each, whether or not they revert, so deliveries for your own requests follow their `reqId` order. If something else may deliver the same asset and amount to the same recipient, use a separate recipient per request to match exactly.

A [Send to Core refund](#send-to-core-refunds) to an EVM-controlled recipient is delivered the same way, for `amount - acceptedAmount * tickSize`.


---

# 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-transfers/evm-and-core.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.
