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);| 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 | routeType | Destination balance: Perps = 0 or Spot = 1. |
- For an ERC-20, approve
EvmInteropManagerto spendamountof the configured EVM token. The contract pulls tokens from the caller. - Call
EvmInteropManager.sendToCore. For ETH,msg.valuemust equalamountexactly. For an ERC-20,msg.valuemust be zero. - Keep the returned
reqIdto 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
Perpsand 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
toon EVM. An ERC-20 refund passes throughEvmInteropManager. An ETH refund is a plain value transfer toto. - L1-controlled recipient: the refund becomes a claim owned by the L1 address
toinEvmL1RefundClaimer. 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:
| 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 releases or mints the tokens to to. | 350,000 |
| L1-controlled | ETH | EvmL1RefundClaimer.receiveL1AccountRefund receives the value and records the claim. | 100,000 |
| L1-controlled | ERC-20 | EvmInteropManager.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.
WarningIf 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:
| Parameter | Value |
|---|---|
target | The EvmL1RefundClaimer 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 | Submit a signed Core transaction. |
| L1 contract call | Call L1CoreMessaging.sendToEvm from the controlling L1 address. |
| EVM contract call | Call 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
| Field | Description |
|---|---|
chainId | Core chain ID, 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: 1 <= baseAmount <= 2^60 - 1. |
usdcFee | Authorized 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:
| Condition | Required 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
baseAmountof 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 coverbaseAmount + usdcFee. - The sequencer sets the fee required to accept the transaction, and
usdcFeemust match it. The sequencer rejects a transaction whoseusdcFeedoes 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
| 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 | routeType | Source balance: Perps = 0 or Spot = 1. |
uint64 | baseAmount | Amount 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);| Type | Parameter | Description |
|---|---|---|
uint48 | accountIndex | Core account to debit. |
uint32 | assetIndex | Asset identifier. |
address | evmRecipient | Recipient address on EVM. |
RouteType | routeType | Source balance: Perps = 0 or Spot = 1. |
uint64 | baseAmount | Amount 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:
| 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.
WarningIf 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:
| 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, 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.
Updated about 7 hours ago
