EVM and Core

EVM to Core

Any EVM address can send funds to a Core master account by calling EvmInteropManager.sendToCore. The EvmInteropManager 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. 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

function sendToCore(
    uint32 assetIndex,
    address to,
    bool isL1Account,
    uint256 amount,
    RouteType routeType
) external payable returns (uint64 reqId);
TypeParameterDescription
uint32assetIndexAsset identifier.
addresstoControlling address of the recipient Core master account.
boolisL1Accounttrue for an L1-controlled Core account; false for an EVM-controlled Core account.
uint256amountAmount in the token's smallest units on EVM, such as wei for ETH. Must be nonzero and a multiple of the asset's tickSize.
RouteTyperouteTypeDestination balance: Perps = 0 or Spot = 1.
  1. For an ERC-20, approve EvmInteropManager to spend amount of the configured EVM token. The contract pulls tokens from the caller.
  2. Call EvmInteropManager.sendToCore. 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 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:

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

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

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. Once Core has processed the request, the read returns status == 1. The response's acceptedAmount gives the amount credited in Core base units, which may be less than the requested amount or zero. See Send to Core for response decoding. Any unaccepted amount is refunded; see 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 reports this as acceptedAmount, expressed in Core base units, 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.
  • 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. 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. The owner must claim it from L1, as described below.

Refund delivery transaction

The Core system address 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:

RecipientAssetEVM deliveryTransaction gas limit
EVM-controlledETHSent directly to to with empty calldata.50,000
EVM-controlledERC-20EvmInteropManager.receiveFromCore releases or mints the tokens to to.350,000
L1-controlledETHEvmL1RefundClaimer.receiveL1AccountRefund receives the value and records the claim.100,000
L1-controlledERC-20EvmInteropManager.receiveL1AccountRefund releases or mints the tokens to EvmL1RefundClaimer 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. A contract at to that receives an ETH refund must accept an ETH transfer with empty calldata within the 50,000 gas limit.

🚧

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.

Claim an L1-controlled recipient's refund

A refund for an L1-controlled recipient is not paid out automatically. EvmL1RefundClaimer, a predeploy 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 on L1 that calls EvmL1RefundClaimer.claimTo on EVM. The claim pays the full accumulated refund balance to an EVM address chosen by the owner.

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) for the claim amount in Core base units and EvmL1RefundClaimer.claimNonces(l1Address, assetIndex) 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 to deliver a claimTo call to EvmL1RefundClaimer on EVM. Use the following parameters:

ParameterValue
targetThe EvmL1RefundClaimer address.
messageABI-encoded claimTo(assetIndex, claimNonce, recipient), where recipient is the nonzero EVM address that receives the funds.
gasLimitGas limit for the claim transaction on EVM.
value0.
refundAddressL1 address that receives any excess msg.value.
msg.valueAt 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 methodHow to initiate it
Signed Core transactionSubmit a signed Core transaction.
L1 contract callCall L1CoreMessaging.sendToEvm from the controlling L1 address.
EVM contract callCall EvmInteropManager.sendToEvm 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 for a JavaScript example using the WASM signer.

Parameters

FieldDescription
chainIdCore chain ID, not the EVM chain ID.
txType128 for Send to EVM.
nonceCore account nonce.
expiredAtCore transaction expiration time as a Unix timestamp in milliseconds.
accountIndexCore account whose balance is debited.
apiKeyIndexIndex of the registered Core API key used to sign the transaction.
assetIndexAsset to transfer; 1 for ETH.
evmRecipientRecipient on Lighter EVM; must not be the zero address.
routeTypeSource balance: 0 for perps; 1 for spot.
baseAmountAmount in Core base units: 1 <= baseAmount <= 2^60 - 1.
usdcFeeAuthorized fee in Core USDC base units (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 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 equals 10^10 wei. See 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:

ConditionRequired signatures
The recipient's EVM-controlled Core 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 and 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 can initiate a transfer by calling L1CoreMessaging.sendToEvm on Ethereum L1. See Chain Info for the contract address.

Call

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

Parameters

TypeParameterDescription
uint48accountIndexCore account to debit: the caller's master account or one of its subaccounts.
uint32assetIndexAsset to transfer; 1 for ETH.
addressevmRecipientRecipient on Lighter EVM; must not be the zero address.
RouteTyperouteTypeSource balance: Perps = 0 or Spot = 1.
uint64baseAmountAmount in Core base units: 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 can initiate a transfer by calling EvmInteropManager.sendToEvm 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.

function sendToEvm(
    uint48 accountIndex,
    uint32 assetIndex,
    address evmRecipient,
    RouteType routeType,
    uint64 baseAmount
) external returns (uint64 reqId);
TypeParameterDescription
uint48accountIndexCore account to debit.
uint32assetIndexAsset identifier.
addressevmRecipientRecipient address on EVM.
RouteTyperouteTypeSource balance: Perps = 0 or Spot = 1.
uint64baseAmountAmount to transfer in Core base units.

Use the returned reqId to check the Core response. 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:

AssetEVM deliveryTransaction gas limit
ETHSent directly to evmRecipient with empty calldata.50,000
EVM-native ERC-20Canonical tokens are released from escrow to evmRecipient.350,000
L1-native ERC-20The 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.

🚧

Warning

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

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, and its fields carry the asset, recipient, and amount even if it reverts:

AssetDelivery transaction
ETHto is the recipient, value is baseAmount * tickSize, and the input is empty. No event is emitted.
ERC-20to is EvmInteropManager, 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 to an EVM-controlled recipient is delivered the same way, for amount - acceptedAmount * tickSize.


Did this page help you?