> 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/message-passing/l1-and-evm.md).

# L1 and EVM

The L1 bridge [`ZkLighter`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol) intermediates messaging between L1 and Lighter EVM.

Arbitrary cross-domain messaging is supported between the two domains.

## L1 to EVM

L1 accounts can submit transactions to Lighter EVM through the bridge's EVM priority queue. The EVM sequencer processes these requests asynchronously in first-in, first-out order.

For example, a caller can request `setValue(42)` on an EVM contract by setting `to` to the target address and encoding the call in `data`.

![Example EVM priority request crossing the asynchronous boundary to a potentially aliased sender on Lighter EVM, which calls setValue(42) on the target contract.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-a0643cbd3eb10bd7353f1812a976437a461a6ca1%2Fl1-evm-priority-request-dark-a0181357.svg?alt=media)

### Submitting an EVM priority request

Call [`ZkLighter.addEvmPriorityRequest(...)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol) on L1 to submit a request.

```solidity
function addEvmPriorityRequest(
    address to,
    uint256 value,
    uint256 depositAmount,
    uint256 gasLimit,
    bool isCreation,
    address refundAddress,
    bytes memory data
) external payable;
```

The parameters define the transaction to execute on Lighter EVM.

* `address to` is the target of the EVM transaction.
* `uint256 value` is the amount of ETH to send to the target with the transaction on EVM. It comes out of the sender's EVM balance after `depositAmount` is credited, so a sender with no EVM balance must set `depositAmount` to at least `value`.
* `uint256 depositAmount` is the amount of ETH to deposit on EVM for the (potentially aliased) `msg.sender`. Once the priority request is accepted on L1, this amount is **guaranteed** to be credited to that EVM address, even if the EVM transaction fails.
* `uint256 gasLimit` is the gas limit for the EVM transaction.
* `bool isCreation` selects whether the transaction is a contract-creation transaction. If `true`, `to` must be `address(0)` and `data` must contain the creation bytecode, including any encoded constructor arguments.
* `address refundAddress` is the address on L1 that will receive any excess `msg.value` after fees and the deposit amount are deducted.
* `bytes memory data` is the transaction's calldata, or its initialization code when `isCreation` is `true`.

`msg.value` must cover at least the [priority request fee](#priority-request-fee-calculation) plus `depositAmount`. Any excess is refunded to `refundAddress`. The fee is forwarded to the protocol treasury.

A successful L1 transaction confirms submission, not successful execution on EVM.

### Priority request fee calculation

Estimate the fee by calling `computePriorityFee(gasLimit)` on the [`ZkLighter`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol) bridge with the gas limit you plan to request.

```solidity
function computePriorityFee(uint256 gasLimit) public view returns (uint256);
```

The fee depends on the base fee (`block.basefee`) and blob base fee (`block.blobbasefee`) of the L1 block that includes the request. These can change before inclusion, so add a buffer to the estimate and send it along with `depositAmount`. Any `msg.value` above the actual fee plus `depositAmount` is refunded to `refundAddress`.

The fee itself is charged in full and is not refunded for unused execution gas or pubdata.

When quoting with `eth_call`, explicitly set a nonzero `gasPrice` at least as high as the selected L1 block's base fee. Otherwise, the node can execute the call with `block.basefee` set to zero and underestimate the fee. No block override is needed to use the selected block's base fee and blob base fee.

With `cast`, pin the quote to the latest block and read its base fee.

```bash
quote_block=$(cast block-number --rpc-url "$L1_RPC_URL")
quote_base_fee=$(cast basefee "$quote_block" --rpc-url "$L1_RPC_URL")
QUOTE_CALLER=0x000000000000000000000000000000000000dEaD
REQUEST_GAS_LIMIT=200000

cast call "$ZK_LIGHTER_BRIDGE" \
  'computePriorityFee(uint256)(uint256)' "$REQUEST_GAS_LIMIT" \
  --rpc-url "$L1_RPC_URL" \
  --block "$quote_block" \
  --from "$QUOTE_CALLER" \
  --legacy \
  --gas-price "$((quote_base_fee + 1))" \
  --gas-limit 100000 \
  --override-balance "$QUOTE_CALLER:1000000000000000000"
```

As in the example, you may also need to override the caller's balance to satisfy the simulation's gas-payment check. This does not fund the account on-chain or spend ETH. Here, `--gas-limit` limits the fee-quote call itself; `REQUEST_GAS_LIMIT` is the gas requested for the EVM priority transaction.

### `msg.sender` of the EVM priority transaction

To prevent contract-address spoofing, contract-senders of EVM priority transactions are aliased on Lighter EVM using the following rule.

```solidity
uint160 constant OFFSET = uint160(0x1111000000000000000000000000000000001111);

function _applyL1ToL2Alias(address sender) internal pure returns (address l2Address) {
    /// @solidity memory-safe-assembly
    unchecked {
        l2Address = address(uint160(sender) + OFFSET);
    }
}
```

Whether aliasing applies is determined at runtime by the L1 check `msg.sender != tx.origin`. EIP-7702 accounts can choose to trigger or avoid aliasing depending on how they submit the request.

The EVM target can authenticate the expected L1 caller as follows.

* **EOA sender without aliasing.** Check `msg.sender == expectedL1Sender`.
* **Contract-sender with aliasing.** Undo the alias before comparing the resulting address with the expected L1 sender.

```solidity
function _undoL1ToL2Alias(address sender) internal pure returns (address l1Address) {
    unchecked {
        l1Address = address(uint160(sender) - OFFSET);
    }
}
```

### Finding the corresponding L2 transaction

Each EVM priority request produces exactly one L2 transaction. Its hash is deterministic, so you can compute it from the corresponding event in the L1 transaction receipt.

1. Decode the `NewEvmPriorityRequest` event emitted by the L1 bridge [`ZkLighter`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol). It has the following fields.

   ```solidity
   event NewEvmPriorityRequest(
       uint64 indexed requestId,
       address indexed sender,
       address indexed to,
       uint256 mintAmount,
       uint256 value,
       uint256 gasLimit,
       bool isCreation,
       bytes data,
       uint64 expirationTimestamp,
       bytes32 prefixHash
   );
   ```
2. Compute `sourceHash = keccak256(prefixHash)`, hashing the 32 raw bytes of the event's `prefixHash`.
3. RLP-encode the fields in the order shown below, prepend the single transaction-type byte `0x7E`, then hash the resulting bytes.

```
l2TxHash = keccak256(0x7E || rlp([
  uint8(1), sourceHash, sender, to, mintAmount, value, gasLimit, isCreation, data
]))
```

### Failure model

An EVM priority request is consumed when its transaction is processed, even if the transaction reverts. Reversion undoes the EVM call's effects, including any transfer of `value` to the target, but does not undo the initiating L1 transaction or the `depositAmount` credited to the sender's EVM address. The request is not retryable.

Some protocols need the EVM action to succeed after an operation has already completed on L1. For example, tokens locked on L1 may still need to be minted on EVM after a failed first attempt. For stronger delivery guarantees, use [`L1ReplayableMessenger`](#l1replayablemessenger) to make the message retryable while preventing duplicate successful execution.

### `L1ReplayableMessenger`

[`L1ReplayableMessenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/replayable/L1ReplayableMessenger.sol) builds on the EVM priority queue and relays messages through [`EvmReplayableMessenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/replayable/EvmReplayableMessenger.sol) on EVM. Together, they handle replayability and abstract away sender aliasing for applications.

![Replayable L1-to-EVM message flow from the EVM priority queue to the aliased L1ReplayableMessenger sender, which calls EvmReplayableMessenger before the target contract.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-49a07e05c158898e476a03cfa8356fdb26838eb2%2Fl1-replayable-message-dark-cf39fa8b.svg?alt=media)

To initiate a replayable message, call `sendMessage(...)` on L1 with the target and encoded call. Make sure to supply the appropriate [priority request fee](#priority-request-fee-calculation) along with `value` as `msg.value`. Any excess is returned to `refundAddress`.

```solidity
function sendMessage(
    address target,
    bytes calldata message,
    uint256 gasLimit,
    uint256 value,
    address refundAddress
) external payable;
```

If delivery fails, anyone can call `replayMessage(...)` on L1 with the original message fields and a new gas limit. Each retry submits a new priority request and pays a fresh priority fee.

```solidity
function replayMessage(
    address _from,
    address _to,
    uint256 _value,
    uint256 _messageNonce,
    address _refundAddress,
    bytes memory _message,
    uint32 _newGasLimit
) external payable;
```

* `_from` is the original caller of `L1ReplayableMessenger.sendMessage(...)`, without aliasing. For a gateway transfer, this is the gateway contract.
* `_to` is the original `target` address on EVM.
* `_value` is the original `value`, in wei. The retry's `msg.value` only needs to cover the new [priority request fee](#priority-request-fee-calculation).
* `_messageNonce` is the original submission's `NewEvmPriorityRequest.requestId`. Reuse it on every retry, even though each retry creates a new priority request.
* `_refundAddress` is the L1 address that receives any excess fee payment for this retry. It may differ from the original refund address.
* `_message` is the original `message` calldata passed to `sendMessage(...)`.
* `_newGasLimit` is the gas limit for the new EVM relay transaction. It can be increased for the retry.

Recover `_from`, `_to`, `_messageNonce`, and `_message` by decoding `relayMessage(...)` from the original `NewEvmPriorityRequest` event's `data` field. Use that event's `value` field for `_value`.

A failed relay leaves the message available for another attempt. Once the target call succeeds, the same message cannot execute again. Replayability does not guarantee eventual success if the target continues to revert.

At the target, `msg.sender` is [`EvmReplayableMessenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/replayable/EvmReplayableMessenger.sol), not the original L1 sender or its alias. For authentication purposes, the target should check both conditions.

* `msg.sender == address(EVM_REPLAYABLE_MESSENGER)`, using the [`EvmReplayableMessenger` predeploy](/evm/chain-info.md#predeploys).
* `EVM_REPLAYABLE_MESSENGER.xDomainSender() == expectedL1Sender`, where `expectedL1Sender` is the authorized L1 address.

### Circumventing Core sequencer censorship for EVM-controlled Core accounts

An EOA controlling an EVM-controlled Core account can use an [L1-to-EVM priority request](#submitting-an-evm-priority-request) to submit a Core operation without relying on ordinary transaction submission to the sequencer. Call [`ZkLighter.addEvmPriorityRequest(...)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol) directly from that EOA on L1, with `to` set to [`EvmToCoreEntrypoint`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmToCoreEntrypoint.sol) and `data` encoding a supported operation such as `cancelAllOrders(accountIndex)`. A direct EOA call preserves the controller's address as `msg.sender` on EVM.

The sequencer is required to process the EVM priority request. Successful execution on EVM submits the write for Core to fulfill asynchronously. Both the priority request and the resulting Core write are covered by the protocol's [escape-hatch conditions](/evm/technical-architecture.md#escape-hatch), so prolonged censorship at either stage allows anyone to activate the escape hatch.

For a contract-controlled Core account, route the priority request through the controlling EVM contract so that it submits the Core write.

## EVM to L1

### Initiating an L1 Call

EVM-originated **L1 Calls** pass through the [`EvmToL1Messenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmToL1Messenger.sol) [predeploy](/evm/chain-info.md#predeploys). It supports [arbitrary calls](#arbitrary-calls) and [token-info reports](#token-info-reports).

#### Arbitrary calls

To initiate an arbitrary **L1 Call**, call `initiateWithdrawal(...)` with the target address and calldata.

![Arbitrary EVM-to-L1 call through EvmToL1Messenger, followed by separate permissionless execution through ZkLighter after batch settlement.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-aa5483f74d156f7eb2ccdc0e6679484ea1e2eaf1%2Fevm-l1-arbitrary-call-dark-13595b19.svg?alt=media)

```solidity
function initiateWithdrawal(
    address _target,
    bytes memory _data
) public payable;
```

**Parameters:**

* `_target` is the address of the L1 contract to call.
* `_data` is the calldata for the L1 call.

**Implicit inputs:**

* `msg.sender` — Exposed to L1 (transiently) as the initiator of the withdrawal.
* `msg.value` — The amount of ETH to withdraw from Lighter EVM. If nonzero, the ETH is burned on the EVM side via a `selfdestruct`-based burner contract.

A successful call to `initiateWithdrawal` emits the following event:

```solidity
enum WithdrawalKind { User, SendTokenInfo }

event WithdrawalInitiated(
    address indexed sender,
    address indexed to,
    uint256 value,
    uint256 nonce,
    WithdrawalKind kind,
    bytes data
);
```

| Field    | Description                                                                                                                                                                                                                                  |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sender` | The initiator of the withdrawal on Lighter EVM (`msg.sender`).                                                                                                                                                                               |
| `to`     | The `_target` passed to `initiateWithdrawal`.                                                                                                                                                                                                |
| `value`  | The `msg.value` sent with the call.                                                                                                                                                                                                          |
| `nonce`  | A monotonically incrementing counter tracking the total number of EVM withdrawals, maintained by the [`EvmToL1Messenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmToL1Messenger.sol). |
| `kind`   | `User` for calls initiated through `initiateWithdrawal(...)`. [Token-info reports](#token-info-reports) use `SendTokenInfo`.                                                                                                                 |
| `data`   | The `_data` passed to `initiateWithdrawal`.                                                                                                                                                                                                  |

#### Token-info reports

Anyone can call [`EvmInteropManager.sendTokenInfo(...)`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol) to request a token-info report for an EVM token.

![Token-info report from EvmInteropManager through EvmToL1Messenger to L1InteropManager, including batch settlement and authenticated execution through ZkLighter.](https://1283879166-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNb0n4Ug3DwUnIWDRnc85%2Fuploads%2Fgit-blob-faa1ce62383f3f6cee1153c57e41a2b5bbd69eee%2Fevm-l1-token-info-dark-9204147f.svg?alt=media)

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

The resulting **L1 Call** targets [`L1InteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/L1InteropManager.sol) and carries the token's ERC-20 metadata and a protocol-supplied admin-slot value. The underlying `EvmToL1Messenger.sendTokenInfo(...)` method accepts calls only from [`EvmInteropManager`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/interop/EvmInteropManager.sol).

See [Token linking](/evm/interop/tokens/token-linking.md#proving-control-of-an-evm-token) for the requirements and full workflow.

### Executing the call on L1

As with [Core-originated **L1 Calls**](/evm/interop/message-passing/l1-and-core.md#core-to-l1), EVM-originated **L1 Calls** become available for execution on L1 once their [batch has been executed](/evm/technical-architecture.md#batch-lifecycle-and-finalization).

However, unlike Core-originated calls, EVM-originated **L1 Calls** are committed to on L1 through a Merkle root. Executing one therefore requires a Merkle proof of its inclusion in the batch, as outlined below.

#### Obtaining the proof

Lighter EVM nodes provide the proof and all execution arguments through `lighter_getWithdrawalProof`. Call this JSON-RPC method on the Lighter EVM RPC endpoint:

```
lighter_getWithdrawalProof(txHash, index)
```

**Parameters:**

| Parameter | Description                                                                                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `txHash`  | Hash of the Lighter EVM transaction that emitted the `WithdrawalInitiated` event.                                                                         |
| `index`   | Zero-based position of the `WithdrawalInitiated` event among those emitted by that transaction. `0` unless the transaction initiated several withdrawals. |

For example, using `curl`:

```bash
curl -s "$LIGHTER_EVM_RPC_URL" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"lighter_getWithdrawalProof","params":["0x<l2 tx hash>", 0]}'
```

The response contains the Merkle proof and all arguments of the L1 claim:

```json
{
  "batchNumber": 1287,
  "proof": [
    "0x9d2f…",
    "0x41c0…"
  ],
  "isLeft": "0x2",
  "l1CallHash": "0x7b3e…",
  "root": "0xe5a1…",
  "l2Sender": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "to": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "value": "0x2386f26fc10000",
  "msgNonce": "0x1c",
  "data": "0x"
}
```

**Response fields:**

| Field                                         | Description                                                                                     |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `batchNumber`                                 | The finalized batch that contains the withdrawal. First argument of `executeEvmL1Call`.         |
| `proof`                                       | The Merkle proof. Empty when the batch contains a single withdrawal.                            |
| `isLeft`                                      | Position of the withdrawal in the batch's Merkle tree, as a hex quantity. Pass it as `uint256`. |
| `l1CallHash`                                  | Hash of the withdrawal message.                                                                 |
| `root`                                        | The batch's Merkle root.                                                                        |
| `l2Sender`, `to`, `value`, `msgNonce`, `data` | The committed **L1 Call** fields used for execution.                                            |

Until the withdrawal has been finalized on L1, the method returns an error. Retry after a few seconds.

#### Submitting the claim

Call `executeEvmL1Call` on the [`ZkLighter`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/ZkLighter.sol) bridge contract on L1:

```solidity
function executeEvmL1Call(
    uint64 batchNumber,
    bytes32[] calldata proof,
    uint256 isLeft,
    address _l2Sender,
    address to,
    uint256 value,
    uint256 msgNonce,
    bytes calldata data
) external;
```

| Parameter     | Source           | Description                                                                                                                                                                            |
| ------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `batchNumber` | proof            | The finalized batch that contains the withdrawal.                                                                                                                                      |
| `proof`       | proof            | The Merkle proof.                                                                                                                                                                      |
| `isLeft`      | proof            | Position of the withdrawal in the batch's Merkle tree.                                                                                                                                 |
| `_l2Sender`   | proof `l2Sender` | The call's authenticated EVM sender. Surfaced via `l2Sender()` during execution.                                                                                                       |
| `to`          | proof `to`       | The target of the L1 call.                                                                                                                                                             |
| `value`       | proof `value`    | The amount of ETH sent with the call.                                                                                                                                                  |
| `msgNonce`    | proof `msgNonce` | The nonce of the withdrawal, as tracked by the [`EvmToL1Messenger`](https://github.com/elliottech/lighter-contracts/blob/lighter-evm-devnet/contracts/messaging/EvmToL1Messenger.sol). |
| `data`        | proof `data`     | The calldata for the L1 call.                                                                                                                                                          |

All eight arguments are contained in the response of `lighter_getWithdrawalProof`.

Anyone can submit the claim; it is not submitted automatically. The ETH and calldata are delivered to `to`, and the submitter pays the L1 gas.

During execution, the target contract must authenticate the calling bridge before reading the original EVM sender through `l2Sender()`.

```solidity
function l2Sender() external view returns (address sender);
```

The `_l2Sender` is stored transiently and is only available for the duration of the call.

If the downstream call reverts, the entire `executeEvmL1Call` transaction reverts and the withdrawal remains unclaimed, so it can be retried with the same proof. Each withdrawal can be claimed only once. Simulating the call with `eth_call` before sending it surfaces the revert reason and confirms that the withdrawal is claimable.


---

# 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/message-passing/l1-and-evm.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.
