> For the complete documentation index, see [llms.txt](https://docs.rome.builders/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rome.builders/developer-guides/call-evm-from-solana.md).

# Call EVM from Solana

Let a Solana wallet drive EVM contracts on Rome — no Ethereum key. The wallet signs, and its transaction goes to Solana.

The reverse of [Call Solana from EVM](/developer-guides/call-solana-from-evm.md): a Solana wallet (for example Solflare) calls any EVM contract on Rome directly, with **no Ethereum key**. This is how [Aerarium](/apps-on-rome/apps.md) lets Solana-native users share the same contracts as EVM users.

The one-call way is the SDK's `submitRomeTxV1`, which has the proxy compose one Solana v1 transaction for the call; this page explains the mechanics.

## Synthetic addresses

A Solana wallet's EVM identity is its **synthetic address** — the last 20 bytes of the `keccak256` of its 32-byte Solana public key:

```
synthetic_evm_address = keccak256(solana_pubkey)[12:]
```

This is the `msg.sender` when the wallet calls a contract, and the stable identity its nonce and storage live under. The derivation is pinned in the protocol, so a wallet always maps to the same address.

```javascript
import { syntheticAddress } from "@rome-protocol/sdk";
const from = syntheticAddress(solanaPubkey); // 0x… 20 bytes
```

## The synthetic is a pass-through — it holds no tokens at rest

A Solana-native user's spendable balance lives in their **Solana wallet** (as SPL tokens), surfaced 1:1 on the EVM side as the token's **ERC20SPL wrapper** (e.g. `wUSDC`). The synthetic holds nothing at rest, so value flows *through* it — every step Solana-wallet-signed:

1. **Provision (once).** A brand-new synthetic's external-auth PDA doesn't exist until `create_pda` runs — and the transfers below are signed by that PDA. `submitRomeTxSolanaLane` provisions it automatically on first use (or call `provisionSynthetic` for an explicit "Activate" step; check with `isSyntheticProvisioned`). With `submitRomeTxV1`, check `isSyntheticProvisioned` and submit `buildCreatePdaCall` through it once.
2. **Fund leg (wallet → synthetic).** Move the token from the wallet's ATA into the synthetic's — the `ActivateAta` step, built by `buildFundLeg`. Now `wrapper.balanceOf(synthetic)` reads that balance, so the synthetic spends it as an ordinary ERC-20 (`transfer` / `transferFrom`) — **not** as native `msg.value`, which it doesn't hold.
3. **The call(s) (`DoTxUnsigned`).** The synthetic runs the EVM transaction(s) — e.g. `approve` then a vault `deposit` that pulls via `transferFrom`.
4. **Sweep leg (synthetic → wallet).** After a withdraw / borrow / claim lands tokens in the synthetic, `buildSweepLeg` pushes them back to the user's own wallet ATA (`HelperProgram.transfer_spl`), so the synthetic nets to nothing.

The next section describes what the older `submitRomeTxSolanaLane` builds client-side. `submitRomeTxV1` has the proxy compose a v1 transaction instead, with no account discovery, ComputeBudget or treasure wallet in your code; see [the external-V1 endpoints](#the-external-v1-endpoints) below.

## How the call is built

`submitRomeTxSolanaLane` does all of this. The mechanics:

1. **Build an unsigned EIP-1559 transaction** with `from` = the synthetic address — no secp256k1 signature.
2. **Discover accounts** — call [`rome_emulateCallAccounts`](/reference/json-rpc.md) with the call's `from`, `to`, `data`, **and `value`** (when the call is payable). Passing `value` matters: the proxy emulates the *real* call, so any value-dependent storage write — e.g. a fresh mapping slot — is allocated and its Solana account is returned. Omit it and that account is missing, and the transaction fails with *"instruction modified data of a read-only account."*
3. **Assemble the Solana transaction:** two **ComputeBudget** instructions (Rome's EVM needs a raised CU limit ≈1.35M and a large heap frame ≈250 KB — Solana's defaults fault) + the **`DoTxUnsigned`** instruction (the unsigned RLP + the discovered accounts) + the per-chain **treasure wallet** (the execution pays it a small fee; discovery omits it, so the SDK appends it).
4. **The wallet signs and submits.** The **Solana wallet signs the Solana transaction (Ed25519) and sends it to the Solana RPC — not to the proxy.** The Solana runtime verifies the signature; on-chain, the program derives `msg.sender` from that signer. Authority is the Solana signature, not an Ethereum one. (The proxy is used only for the emulation/discovery in step 2.)

The user's PDA (`["EXTERNAL_AUTHORITY", synthetic_address]`) owns their token accounts and signs SPL CPIs on their behalf — so a Solana user can supply, borrow, swap, and LP entirely from their Solana wallet.

## The external-V1 endpoints

`submitRomeTxV1` is the one-call way: it asks the proxy for a composed v1 transaction, checks it, has the wallet sign it and sends it to the Solana RPC, relaying through the proxy when the call does not fit. The proxy exposes the same lane as RPC methods, so any client can use it without the SDK (full list in [JSON-RPC Support](/reference/json-rpc.md)):

* `rome_composeUnsignedAtomicV1(rlp, pubkey, signature)` — one Solana v1 transaction; the wallet signs it and the app broadcasts it to the Solana RPC. Refused when the call does not fit one atomic transaction.
* `rome_composeUnsignedIterativeV1(rlp, pubkey, signature)` — every leg of the execution (atomic if it fits, otherwise the iterative legs); the wallet signs each, `rome_submitSignedV1Plan` broadcasts them. The limits on a long plan are in [Relayed calls and long calls](#relayed-calls-and-long-calls).
* `rome_relayUnsignedV1(rlp, pubkey, signature)` — one wallet signature, the proxy's payer drives the legs and returns the EVM tx hash; the synthetic pays Rome gas, so fund it once with `rome_composeExternalDepositV1`.

**The authorisation signature.** `rlp` is the bare 9-field unsigned EIP-1559 body. The wallet signs, with its Ed25519 key, either the raw 32-byte `keccak256(0x02 ‖ rlp)` or the readable message

```
Rome authorization\n0x<64 lowercase hex chars of that hash>
```

Both are accepted; the readable form is what a wallet renders legibly in its `signMessage` prompt. The program recomputes the hash from the body it executes and requires the signed message to be exactly one of the two forms, so a signature over one body can never authorise another, and the body's nonce prevents replay.

## Relayed calls and long calls

Some calls do not fit one Solana transaction. This section covers what happens then, and what limits apply.

### When the atomic composer refuses

`rome_composeUnsignedAtomicV1` refuses a call that does not fit one atomic transaction. `submitRomeTxV1` then relays the call through the proxy with the **same** authorization signature, so the wallet is not asked a second time. The result has `route: "relay"` and no Solana `signature`. Pass `relayOnRefusal: false` to get the refusal as an error instead, or call `relayV1` yourself to relay on purpose.

Three things to know about a relayed call:

* **The synthetic pays the gas.** On the relay route the proxy submits and pays for the Solana transactions, and the call is paid for in the chain's native gas token from the synthetic's balance. The balance must be at least `gasLimit × maxFeePerGas`. `fundSyntheticGas(deps, amountWei)` tops it up from the wallet's own token account of the chain's gas token (USDC on Hadrian and Rubicon); the wallet signs one plain v1 transaction, and needs a little SOL for the Solana fee. `amountWei` is in the chain's 18-decimal wei and must be a multiple of 10^12, or the proxy refuses it. Without the SDK, `rome_composeExternalDepositV1` builds the same transaction.
* **A revert does not throw.** A relayed call that reverts on the EVM side resolves normally, with `receipt.status === "0x0"`. Check `receipt?.status === "0x1"` before treating the call as done. `receipt` is `null` if the receipt did not appear in time.
* **`relayOnRefusal: false` is a choice made in the SDK, not a lock.** The proxy already holds the authorization signature for that payload and can run it by either route, up to `gasLimit × maxFeePerGas`.

```javascript
const res = await submitRomeTxV1(deps, { to, data, value });
if (res.route === "relay" && res.receipt?.status !== "0x1") {
  // a relayed call that reverted resolves; it does not throw
}
```

### Long calls

A call that needs more than one transaction runs as an iterative session: the program executes as many EVM steps as fit in each Solana transaction and saves its state between them. Two ways to drive it from a Solana wallet:

* **Relay it.** `rome_relayUnsignedV1` takes one wallet signature and the proxy's payer sends every leg. The synthetic pays the gas, as above.
* **Sign the legs yourself.** `rome_composeUnsignedIterativeV1` returns every leg with an empty signature slot, the wallet signs each one, and `rome_submitSignedV1Plan` broadcasts them in order. Each leg is a separate Solana transaction for the wallet to sign, so a wallet that signs one transaction at a time prompts once per leg, on top of the authorization signature. The wallet pays the Solana fee on every leg.

The limits:

| Limit                                              | Value              | Where it comes from                                                                                                     |
| -------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Transactions in one `rome_submitSignedV1Plan` call | 64                 | The proxy refuses a longer plan                                                                                         |
| Legs the iterative composer will plan              | 128                | The cap in the Rust Rome SDK's iterative composer, which also bounds the plan `rome_composeUnsignedIterativeV1` returns |
| Iterations in one session                          | 160                | The Rome EVM program; past it the session stops resuming, releases its account locks and ends                           |
| Solana transaction size and accounts               | 4 KB, 64 addresses | Every leg is a Solana v1 transaction                                                                                    |

Most real calls settle in far fewer legs than these caps. The 64-transaction limit on a wallet-signed plan is the one you will meet first. A call that needs more legs than that cannot be broadcast through `rome_submitSignedV1Plan`.

A long call cannot make legacy-track CPIs. Mutating calls on the legacy precompiles (`0xff…08`, `0xff…09` and `0x42…16`) are refused in an iterative session with `CpiProhibitedInIterativeTx`. Reads of Solana state and the cached-track precompiles (`0xff…04`, `0xff…05`, `0xff…06`, `0xff…0b`) work. A call that makes legacy-track CPIs must fit one atomic transaction, whichever lane sends it. See [Constraints](/core-concepts/constraints.md) and [Compute Budget](/core-concepts/compute-budget.md).

## Reference implementations

* **Aerarium** — drives Compound v3 (supply / borrow) from a Solana wallet via `DoTxUnsigned`. It is the reference for the older legacy-transaction lane described above, not for `submitRomeTxV1`.
* **Rome DEX** is not an example of this page: its Solana side is a native Solana program that the wallet signs for directly, and its EVM side shares the same pool.

Aerarium needs no bridge or second wallet — the synthetic address makes one Solana key a first-class EVM account. For the v1 pattern, follow [Build a dual-lane app](/developer-guides/dual-lane-app.md).

## What's Next

* [Call Solana from EVM](/developer-guides/call-solana-from-evm.md) — the other direction, via CPI
* [Token Interop](/core-concepts/token-interop.md) — how balances are shared across EVM and Solana


---

# 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.rome.builders/developer-guides/call-evm-from-solana.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.
