# 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
This site has an agentic ask interface you may use to query the documentation dynamically 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/index.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.

---

# Getting Started

EVM execution on Solana via Cross-Program Invocation.

Rome Protocol is an EVM execution environment running natively inside the Solana runtime. Deploy Solidity smart contracts on Solana with atomic CPI access to Solana programs and liquidity.

## What Makes Rome Different

* **Single State** — EVM contracts and Solana programs share the same state. No bridging, no sync delays.
* **CPI Access** — Solidity contracts call any Solana program directly, from SPL Token to Meteora.
* **Standard Tooling** — Deploy with Hardhat or Foundry. Interact with MetaMask. Write Solidity.
* **App Sovereignty** — Each app gets its own EVM chain with custom gas token and fee revenue.

## For Developers

* [What is Rome?](/getting-started/what-is-rome) — How EVM execution works on Solana
* [Quickstart](/getting-started/quickstart) — Deploy your first contract in under 5 minutes
* [Deploy Solidity](/developer-guides/deploy-solidity) — Hardhat and Foundry deployment guides
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — CPI from Solidity to Solana programs
* [Rome SDK](/products/rome-sdk) — Build against Rome from Solidity and Rust

## Apps on Rome

Rome is live software. See what's running today — DEX, lending, bridge, explorer, and more — in [Apps on Rome](/apps-on-rome/apps).

## Products

* [App Sovereignty](/products/app-sovereignty) — Launch your own EVM chain on Solana
* [Oracle Gateway](/products/oracle-gateway) — Pyth/Switchboard as Chainlink AggregatorV3Interface
* [Rome SDK](/products/rome-sdk) — Build against Rome from Solidity and Rust

## Core Concepts

* [Architecture](/getting-started/architecture) — System overview and component diagram
* [Execution Model](/core-concepts/execution-model) — Atomic vs iterative execution
* [Token Interop](/core-concepts/token-interop) — How ERC-20 and SPL tokens work together
* [Constraints](/core-concepts/constraints) — Important limits and boundaries

## Reference

* [Contract Addresses](/reference/contract-addresses) — Deployed addresses per chain
* [Glossary](/resources/glossary) — Rome-specific terminology
* [FAQ](/resources/faq) — Common questions answered

## Networks

Build against **Hadrian** (testnet) and ship to **Rubicon** (mainnet, real USDC). Full details, wallet setup, and funding are on the [Networks](/networks/networks) page.

| Network                                | Chain ID | RPC URL                                     |
| -------------------------------------- | -------- | ------------------------------------------- |
| [Rubicon](/networks/rubicon) (mainnet) | 7531     | `https://rubicon.mainnet.romeprotocol.xyz/` |
| [Hadrian](/networks/hadrian) (testnet) | 200010   | `https://hadrian.testnet.romeprotocol.xyz/` |

## Need Help?

* [Discord](https://discord.gg/vZ9rnCdNSB) — Developer community and support
* [Telegram](https://t.me/+tdnr-M6kcngxYzhk) — Updates and announcements
* [GitHub](https://github.com/rome-protocol) — Source code and issues


# What is Rome?

Rome is an EVM execution environment inside Solana. Whatever you build is open to EVM users, Solana users, and users on other chains. Four ways to build.

Rome is an EVM execution environment that runs inside Solana. Whatever you build on Rome is open to everyone at once — EVM users, Solana users, and users on other chains. You build once, and each of them can reach it.

## How Rome works

Rome embeds a full EVM bytecode interpreter as a Solana on-chain program. When you deploy a Solidity contract on Rome, it lives on Solana; when it runs, it runs inside Solana's runtime with direct access to any Solana program — SPL Token, Jupiter, Kamino, Meteora, or your own program — atomically, in one transaction. EVM and Solana share the same state, so a token on Solana and its ERC-20 form in the EVM are the same account: nothing is bridged or wrapped.

Because it all runs on Solana, **either wallet drives the same apps.** A MetaMask user signs an EVM transaction the usual way. A Phantom user signs with their **Solana** key — no EVM keypair, no separate address — and uses the very same Solidity apps directly. Both reach the same contracts and the same state, each with the identity they already have.

```mermaid
flowchart LR
    MM["MetaMask (EVM key)"] --> APPS
    PH["Phantom (Solana key)"] --> APPS
    APPS["Rome: your Solidity apps<br/>one shared state, inside Solana"] --> PROGS["Solana programs<br/>Jupiter, Meteora, Pyth"]
```

## How to use Rome

There are four ways to build on Rome. Each has a guide, and working code you can read.

### Bring your Solidity

Deploy your existing Solidity contracts as they are. On Rome they can call Solana programs atomically — a swap, a price read — inside one transaction, and the same contract is open to EVM wallets, Solana wallets, and users arriving from other chains.

**How to build it:** [Deploy Solidity](/developer-guides/deploy-solidity), then [Call Solana from EVM](/developer-guides/call-solana-from-evm).

**Examples to read:** Compound v3 and Aave v3 run on Rome unchanged — [compound-on-rome-comet](https://github.com/rome-protocol/compound-on-rome-comet), [rome-aave-v3](https://github.com/rome-protocol/rome-aave-v3). [Aerarium](https://github.com/rome-protocol/aerarium) is a lending app where EVM and Solana users share one market.

### Bring your Solana program

Your Solana program keeps doing what it already does for your Solana users. On Rome it also becomes reachable from Solidity, so EVM users — and users on other chains — can use it too, with no changes to the program.

**How to build it:** [Call a Solana program from EVM](/developer-guides/call-solana-from-evm); [Call EVM from Solana](/developer-guides/call-evm-from-solana) covers the other direction.

**Examples to read:** [cardo](https://github.com/rome-protocol/cardo) reaches Jupiter, Meteora, Marinade, and Mango from an EVM account.

### Bring your idea

Start something new that uses both sides from the beginning — one pool that EVM and Solana users share, a market both sides borrow from, an oracle that brings Solana prices into the EVM. Every audience can use it from day one.

**How to build it:** first [choose your core](/core-concepts/choose-your-core), then [Call Solana from EVM](/developer-guides/call-solana-from-evm) and the [DeFi patterns](/use-cases/defi-protocols).

**Examples to read:** [rome-dex](https://github.com/rome-protocol/rome-dex) is one pool traded by both EVM and Solana wallets; [rome-oracle-gateway](https://github.com/rome-protocol/rome-oracle-gateway) reads Solana price feeds and serves them to Solidity through a standard Chainlink interface.

### From home

Your users don't have to move. From their home chain — Arbitrum, Monad, or any other chain — they can reach a Rome app and get to Solana without leaving where they already are.

**How to build it:** [From home: reach Rome from another chain](/developer-guides/from-home).

**Examples to read:** [appia](https://github.com/rome-protocol/appia) — users bring USDC from their home chain and use Rome-side DeFi; [rome-bridge-api](https://github.com/rome-protocol/rome-bridge-api) is the bridge orchestrator.

## Run it on your own chain

When you're ready, you can run any of this on your own Rome chain, with your own gas token and fees that accrue to your app. See [App Sovereignty](/products/app-sovereignty).

## Prerequisites

* Solidity experience (Hardhat or Foundry), or a Solana program to bring
* A wallet — MetaMask and/or a Solana wallet like Phantom
* A basic grasp of Solana's account model (see [Key Concepts](/getting-started/key-concepts))

## What's Next

* [Quickstart](/getting-started/quickstart) — deploy your first contract in minutes
* [Architecture](/getting-started/architecture) — how EVM execution works inside Solana
* [Apps on Rome](/apps-on-rome/apps) — the full catalog of live apps and their repos


# Why Rome?

How Rome differs from other EVM-on-Solana approaches — atomic CPI composability, single shared state, and sovereign app chains with custom gas tokens.

Several projects bring EVM compatibility to Solana. Rome takes a fundamentally different approach.

## Core Differentiators

### 1. Atomic Composability via CPI

Rome is the only EVM environment where Solidity contracts can call Solana programs within the same transaction. A single Solidity function can:

```solidity
// All in one atomic transaction:
int256 price = IPyth(PYTH).getPrice("SOL/USD");        // Read Pyth oracle
uint256 quote = IJupiter(JUPITER).getQuote(USDC, SOL);  // Get Jupiter quote
IJupiter(JUPITER).swap(USDC, SOL, amount, minOut);       // Execute swap
IKamino(KAMINO).deposit(SOL, collateralAmount);          // Supply to Kamino
```

If any step fails, the entire transaction reverts. No partial execution, no race conditions.

### 2. Single State

On Rome, an SPL token and its ERC-20 representation are the same underlying account. When a Solidity contract transfers USDC, it's moving the actual SPL USDC — not a wrapped copy.

This means:

* **No liquidity fragmentation** — Solana DeFi and EVM DeFi share the same pools
* **No bridging risk** — there's no bridge to exploit because there's no bridge
* **Real-time composability** — EVM contracts see Solana state changes immediately

### 3. App Sovereignty

Each application on Rome gets its own EVM environment:

* **Custom chain ID** — your app is its own chain
* **Custom gas token** — any SPL token, priced via Meteora LP pool
* **Gas revenue** — fees accrue to your application, not Rome protocol
* **Full EVM tooling** — your users connect MetaMask, your devs use Hardhat

## When to Use Rome

**Use Rome when:**

* You want EVM contracts with direct access to Solana DeFi
* You want a sovereign EVM chain on Solana with your own gas token
* You're porting Ethereum contracts that need Solana's speed and liquidity
* You need atomic operations across EVM and Solana programs

**Consider alternatives when:**

* You only need standard Solana programs (use native Solana/Anchor)
* You need Ethereum mainnet settlement (use a traditional L2)
* You need cross-chain messaging without EVM execution (use Wormhole/Hyperlane directly)

## What's Next

* [Architecture](/getting-started/architecture) — how Rome EVM works inside Solana
* [Quickstart](/getting-started/quickstart) — deploy your first contract


# Architecture

How Rome's components fit together — EVM execution inside a Solana program.

Rome embeds an EVM bytecode interpreter inside a Solana on-chain program. In the EVM-key lane, users send signed EVM transactions to the Rome Proxy over standard Ethereum JSON-RPC. The proxy submits them to the Rome EVM program on Solana. One EVM transaction can require several Solana transactions to write chunks into holder accounts or run iterative execution. In the v1 Solana-key lane, the proxy composes the transaction. The Solana wallet signs it and sends it directly to Solana RPC. There is no separate execution layer to keep in sync — EVM state *is* Solana state.

## System overview

<figure><img src="https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-4d7d386c95b500079204f3dea2103d9759c5de8f%2Farchitecture.svg?alt=media" alt="EVM wallet and Solana wallet connected to Rome Proxy with its emulator. The Solana cluster holds the Rome EVM program, per-chain state, external-authority PDA, token accounts, Solana programs and holder accounts. Hercules feeds blocks, receipts and logs back to the proxy."><figcaption></figcaption></figure>

## Components

### Rome EVM Program (on-chain)

The core of Rome — a Solana BPF program containing a full EVM bytecode interpreter (a fork of SputnikVM). It:

* Receives signed EVM transactions and unsigned EIP-1559 bodies as Solana instructions. A Solana key authorizes an unsigned body through the Solana fee payer's signature or an Ed25519 verifier instruction
* Executes Solidity bytecode inside the Solana runtime
* Maps each Ethereum address (H160) to a balance PDA per chain containing its nonce, native balance and code. Separate storage PDAs are associated with the balance PDA. Each address also has a program-derived address for authority over Solana calls, called its **external-authority PDA**. This PDA is shared by every chain on the program. It owns the address's SPL token accounts and signs its cross-program invocations (CPIs)
* Exposes precompiles for calling into Solana (CPI, System, Helper, Withdraw) alongside Ethereum precompiles. The cached family adds SystemCached (`0xff…04`), SplCached (`0xff…05`), ASplCached (`0xff…06`) and WithdrawCached (`0xff…0b`). The factory deploys wrappers on this cached track
* Stores EVM state (balances, nonce, code, storage) as Solana account data
* One Rome EVM program hosts many chain IDs. The chain ID is part of the balance and storage PDA addresses, not of the external-authority PDA

### Rome Proxy (JSON-RPC server)

The Rome Proxy is a standard Ethereum JSON-RPC server. Users connect to `https://hadrian.testnet.romeprotocol.xyz/` for Hadrian (testnet) and `https://rubicon.mainnet.romeprotocol.xyz/` for Rubicon (mainnet). It translates Ethereum API calls into Solana activity:

* `eth_sendRawTransaction` → submit one or more Solana v1 transactions: atomic `DoTx`, holder chunks (`TransmitTx`) plus `DoTxHolder`, or iterative legs. A pooled payer signs and pays SOL. A pooled fee recipient collects the user's gas
* `eth_call` → run the EVM interpreter natively in the emulator on live Solana accounts read over RPC. Mollusk runs the Solana programs reached by CPI. It also runs the Rome EVM program when the proxy measures compute units
* `eth_estimateGas` → simulate for gas estimation. A gas unit is one lamport of Solana cost. The estimate is never below 21,000
* `eth_getBalance`, `eth_getCode`, `eth_getTransactionCount`, `eth_getStorageAt` → read live Solana accounts through the emulator. `eth_getBlockByNumber`, receipts and logs come from Hercules

It also exposes Rome extensions: `rome_emulateTx`, `rome_emulateRegRollup`, `rome_mintId`, `rome_buildInfo` and `rome_getResources`. Solana-wallet methods include `rome_emulateCallAccounts`, `rome_composeUnsignedAtomicV1`, `rome_composeUnsignedIterativeV1`, `rome_submitSignedV1Plan`, `rome_relayUnsignedV1`, `rome_composeExternalDepositV1` and `rome_sendUnsignedTransaction`. The proxy also exposes `rome_solanaTxForEvmTx`.

### Hercules (indexer)

Hercules watches the Rome EVM program on Solana and reconstructs Ethereum-compatible blocks in PostgreSQL. These contain transactions, receipts, logs and state changes. On the public chains it runs in slot-aligned mode, so a block number maps to a Solana slot and the same slot yields the same block on any indexer. The proxy serves these blocks to wallets and explorers.

Hercules indexes confirmed blocks. It re-checks each block at finalized and reloads it if the blockhash changed. The [Via explorer](/apps-on-rome/via) reads the Hercules data.

### Precompiles

Rome implements ecrecover, SHA-256, RIPEMD-160, identity, the BN254 curve operations and blake2f with Ethereum semantics, except for failure data from BN254 and blake2f. A failed BN254 or blake2f call returns Rome's `Error(string)` revert data instead of an empty buffer. Modexp (`0x05`) is disabled and reverts with `"big_mod_exp call is disabled"`. blake2f refuses more than 1,000,000 rounds, which Ethereum allows.

Non-EVM precompiles reach into Solana: **CpiProgram** (`0xFF…08`, arbitrary CPI), **System** (`0xFF…07`, PDA derivation and base58 helpers), **HelperProgram** (`0xFF…09`, ATA/PDA creation, SPL transfers, gas↔lamports), and **Withdraw** (`0x42…16`). The cached family is **SystemCached** (`0xff…04`), **SplCached** (`0xff…05`), **ASplCached** (`0xff…06`) and **WithdrawCached** (`0xff…0b`). Legacy CPIs run immediately and only in atomic transactions. Cached CPIs are recorded during execution and sent at commit. They revert with the EVM. They can run in iterative transactions. See the [Contract Addresses](/reference/contract-addresses) reference for the full table.

## Execution modes

Rome executes an EVM transaction in one of two ways:

### Atomic (VmAt)

A single Solana transaction. The whole EVM transaction executes within one Solana transaction's compute budget (\~1.4M compute units). Used for most operations — transfers, ordinary contract calls, swaps.

### Iterative (VmIt)

For work that exceeds a single transaction's budget. Execution is split across multiple Solana transactions:

1. Each step (a Solana transaction) runs as many EVM opcodes as fit in its compute budget — the step size is adaptive, not fixed
2. VM state is Borsh-serialized into a `StateHolder` account between steps
3. Accounts are TTL-locked for a few seconds during execution

The proxy falls back to iterative execution when compute units, heap or instruction count do not fit in one Solana transaction. A session is capped at 160 iterations. A legacy-track CPI is refused in an iterative transaction, because it cannot be split across legs. Cached-track CPIs are allowed.

## Account mapping

Every Ethereum address maps deterministically to a Solana balance PDA derived from the chain ID and the address under the Rome EVM program. Its native gas balance is a field in that PDA, alongside the nonce and contract code. Storage slots live in separate storage PDAs. SPL balances live in token accounts owned by the separate external-authority PDA. That authority is shared across every chain on the same program.

## Two ways in

* **EVM key lane:** a wallet sends a secp256k1-signed EVM transaction to the Rome Proxy. The proxy submits the Solana transactions.
* **Solana key lane:** a wallet authorizes an unsigned EIP-1559 body with its Solana key. For the direct v1 route, the proxy composes the Solana transaction. The wallet signs it and submits it directly to Solana RPC. The relay route uses the proxy to submit instead. The older legacy route has the wallet build, sign and pay for a `DoTxUnsigned` Solana transaction, and the program derives the sender from the Solana signer.

A Solana public key maps to the synthetic address `keccak256(pubkey)[12:]`, the last 20 bytes of the hash. See [How Solana and the EVM meet on Rome](/core-concepts/how-solana-and-evm-meet).

## Who signs a call into Solana

The Rome EVM program signs a CPI with the external-authority PDA of the immediate caller of the precompile. A contract signs as itself. To spend a user's SPL tokens, the contract needs an SPL delegation from that user. The user grants it with `HelperProgram.approve_spl(spender, amount, mint)`. A direct call from the user's EVM or synthetic address to the precompile uses the user's external-authority PDA.

## Holder accounts

Solana (v1) transactions are capped at 4 KB, but EVM transactions (especially contract deployments) can be much larger. Rome splits large transactions into 3,600-byte chunks and writes them sequentially into **holder accounts** (up to 80 KB). It then assembles each transaction and executes it on-chain. The Rome SDK manages this transparently. Each chunk's signature fee is billed to the sender's gas.

## Gas and pricing

Each chain has its own gas token: SOL or an SPL token. The price source is Hermes API, a Meteora DAMM v1 pool, a Meteora DAMM v2 pool or a fixed price. It provides the conversion between the gas token and SOL for the underlying Solana transaction fees.

* A gas unit is one lamport of Solana cost
* `eth_gasPrice` is gas-token wei per lamport
* The sender pays the fee recipient from its native gas balance. The proxy payer pays SOL
* With no fee recipient, as in the Solana-wallet atomic lane, no EVM gas is charged. The wallet pays SOL directly
* The native balance has 18 decimals. It is backed by gas-token SPL held in a per-chain program wallet

## Finality

Rome uses Solana's finality. The SDK waits for `confirmed`. Hercules re-checks each indexed block at `finalized` and reloads it if the blockhash changed.

## What's next

* [Networks](/networks/networks) — connect to Hadrian (testnet) and Rubicon (mainnet)
* [Quickstart](/getting-started/quickstart) — deploy your first contract
* [Execution Model](/core-concepts/execution-model) — atomic vs iterative execution in depth
* [Token Interop](/core-concepts/token-interop)
* [Constraints](/core-concepts/constraints)
* [Call Solana from EVM](/developer-guides/call-solana-from-evm)
* [Call EVM from Solana](/developer-guides/call-evm-from-solana)
* [Precompile reference](/reference/precompiles)
* [How Solana and the EVM meet on Rome](/core-concepts/how-solana-and-evm-meet)


# Ecosystem & repos

Every public Rome repo, what it is, and how they compose — the map for navigating the surface.

Rome's public surface is a set of repositories: a few **foundation** packages you build on, a scaffolder, reference apps to learn from, and two services your app calls. This page is the map — what each repo is and how they fit together.

Two neighbours answer different questions, and this page links to both rather than repeating them:

* [Architecture](/getting-started/architecture) explains the **protocol** — how the EVM runs inside a Solana program.
* Each repo ships an **`AGENTS.md`** (e.g. [rome-sdk-ts/AGENTS.md](https://github.com/rome-protocol/rome-sdk-ts/blob/main/AGENTS.md)) that routes by **what you're starting from** (a Solidity contract, a Solana program, greenfield, from-home) to the one example closest to your case.

This page is the level in between: the **whole surface** and how the pieces connect.

## The surface at a glance

### Foundation — build on these

| Repo                                                            | What it is                                                                                                                                                  | Reach for it when                                |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| [rome-registry](https://github.com/rome-protocol/rome-registry) | `@rome-protocol/registry` — read-only, generated projection of live chain facts (ids, RPC, addresses, token mints, program ids, oracle feeds, ALTs).        | You need a real chain fact — never hardcode one. |
| [rome-sdk-ts](https://github.com/rome-protocol/rome-sdk-ts)     | `@rome-protocol/sdk` — the TypeScript write path: `submitRomeTx` + fee sizing, both lanes, PDA/ATA + CPI encoders, precompile bindings, a `/bridge` client. | You're writing app or frontend code.             |
| [rome-solidity](https://github.com/rome-protocol/rome-solidity) | Solidity precompile interfaces, SPL/ERC-20 wrappers, and the oracle adapters — the contract-side toolkit.                                                   | You're writing contracts.                        |

The `@rome-protocol/…` names above are package names, but the packages are not published to npm. Install them from GitHub (the commands are in each section under [The foundation layer, up close](#the-foundation-layer-up-close)); the import paths in your code stay the same.

### Rome ZK EVM rollups

| Repo                                                            | What it is                                          | Reach for it when                                            |
| --------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| [solana-zk-evm](https://github.com/rome-protocol/solana-zk-evm) | ZK rollup programs, node, prover and operator docs. | You're running an EVM chain that settles on Solana.          |
| [rome-zk-guest](https://github.com/rome-protocol/rome-zk-guest) | Batch guest source built for each chain's genesis.  | You're building a guest and requesting its verification key. |

### Canton ZK EVM

| Repo                                                                                                                    | What it is                                                                                                      | Reach for it when                                                                                                          |
| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| [canton-zk-evm](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/README.md) | Ostia, an early release of a Canton zkEVM that settles proven EVM blocks and Canton actions in one transaction. | You're running the local demo or studying its gateway and proof checks. See [Canton ZK EVM](/canton-zk-evm/canton-zk-evm). |

### Reference apps — learn by example

| Repo                                                                    | What it is                                                                                | Reach for it when                                    |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [rome-dex](https://github.com/rome-protocol/rome-dex)                   | Dual-lane AMM — a native Solana pool with a thin EVM router.                              | Opening the other lane; a **native-core** example.   |
| [aerarium](https://github.com/rome-protocol/aerarium)                   | Dual-lane lending — a Solidity Comet core that Solana users reach via a synthetic sender. | Opening the other lane; a **Solidity-core** example. |
| [cardo](https://github.com/rome-protocol/cardo)                         | EVM users driving Solana dApps (swap/stake/lend/perps) via CPI.                           | A worked CPI-to-Solana app to study.                 |
| [appia](https://github.com/rome-protocol/appia)                         | Positions-first cross-VM DeFi; users reach it from their home chain.                      | A **from-home** app.                                 |
| [rome-aave-v3-demo](https://github.com/rome-protocol/rome-aave-v3-demo) | Aave v3 supply/borrow/repay, deployed unchanged.                                          | A full EVM app running as-is.                        |

### Contract forks — known protocols, deployed on Rome

| Repo                                                                              | What it is                         | Reach for it when             |
| --------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------- |
| [compound-on-rome-comet](https://github.com/rome-protocol/compound-on-rome-comet) | Compound III (Comet) money market. | Forking a known EVM protocol. |
| [rome-aave-v3](https://github.com/rome-protocol/rome-aave-v3)                     | Aave v3 contracts fork.            | Forking a known EVM protocol. |

### Services — your app calls these

| Repo                                                                        | What it is                                                                                       | Reach for it when                      |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------- |
| [rome-bridge-api](https://github.com/rome-protocol/rome-bridge-api)         | Off-chain orchestrator + fee-sponsor for the on-chain bridge. Holds no keys.                     | Funding a wallet, or a from-home flow. |
| [rome-oracle-gateway](https://github.com/rome-protocol/rome-oracle-gateway) | Solana price feeds (Pyth, Switchboard) exposed to EVM via the Chainlink `AggregatorV3Interface`. | You need a price feed.                 |

### Scaffold — start here

| Repo                                                                | What it is                                                                                                                             | Reach for it when   |
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| [create-rome-app](https://github.com/rome-protocol/create-rome-app) | `npx github:rome-protocol/create-rome-app` — scaffolds a dual-lane app pre-wired to the registry + SDK, with a funded both-lane check. | Starting a new app. |

## How they compose

```mermaid
flowchart TD
    REG["rome-registry — live chain facts"]
    SDK["rome-sdk-ts — writes, CPI, bridge client"]
    SOL["rome-solidity — precompile interfaces, SPL wrappers, oracle adapters"]
    CRA["create-rome-app — scaffolder"]
    APP["your app — frontend + contracts"]
    BAPI["rome-bridge-api — off-chain orchestrator, holds no keys"]
    OG["rome-oracle-gateway — Solana price feeds"]

    CRA -->|scaffolds, pre-wired| APP
    REG -->|facts, never hardcoded| APP
    SDK -->|write path & CPI| APP
    SOL -->|imported by your contracts| APP
    APP -->|SDK /bridge client| BAPI
    APP -->|reads via AggregatorV3Interface| OG
    OG -.->|adapters live in| SOL
    CRA -.->|wires| REG
    CRA -.->|wires| SDK
```

The spine is three foundations and a scaffolder:

* [**rome-registry**](https://github.com/rome-protocol/rome-registry) is the single source of live facts. [**rome-sdk-ts**](https://github.com/rome-protocol/rome-sdk-ts) is the write path — and its `/bridge` subpath is the client your app uses to talk to `rome-bridge-api`. [**rome-solidity**](https://github.com/rome-protocol/rome-solidity) is what your contracts (and the forks) import; the oracle adapters that `rome-oracle-gateway` deploys live there too. The three are independent of each other — pick the ones your app needs.
* [**create-rome-app**](https://github.com/rome-protocol/create-rome-app) ties the first two together for a new app, so a fresh project starts already reading the registry and writing through the SDK.

The reference apps are the exception worth knowing: **they predate the extracted packages.** cardo consumes the registry package; appia projects the registry to static JSON at build time; and both vendor their write path rather than importing the SDK. Read them to learn the patterns — but a **new** app should start from `create-rome-app` + the packages, not by cloning an app.

## Navigate by what you need

* **A live chain fact** (id, address, mint, program id) → [rome-registry](https://github.com/rome-protocol/rome-registry). Never hardcode.
* **Writing app code** (a write, a CPI call, a bridge) → [rome-sdk-ts](https://github.com/rome-protocol/rome-sdk-ts); reference at [Rome SDK](/products/rome-sdk).
* **Writing contracts** → [rome-solidity](https://github.com/rome-protocol/rome-solidity); see [Deploy Solidity Contracts](/developer-guides/deploy-solidity).
* **Calling a Solana program from a Solidity contract** → the CPI precompile in [rome-solidity](https://github.com/rome-protocol/rome-solidity) (`interface.sol`); [cardo](https://github.com/rome-protocol/cardo) is a worked example.
* **A working example** of an AMM, lending, CPI, or from-home app → the reference apps above; each repo's `AGENTS.md` routes by starting point.
* **Scaffolding a new app** → [create-rome-app](https://github.com/rome-protocol/create-rome-app).
* **A price feed** → [rome-oracle-gateway](https://github.com/rome-protocol/rome-oracle-gateway) (see the [Oracle Gateway portal](/apps-on-rome/oracle-gateway)).
* **Users on another chain** → [From home](/developer-guides/from-home) + [rome-bridge-api](https://github.com/rome-protocol/rome-bridge-api).
* **Deciding which side holds your logic** → [Choose your core](/core-concepts/choose-your-core).

## The foundation layer, up close

### rome-registry — live facts, public by construction

The registry is **generated** from Rome's internal source: an allowlist emits only what's meant to be public, **substitutes** internal endpoints for their public equivalents, and **default-denies** everything else — so it's public-safe by construction. It publishes, per chain, `chain.json` / `tokens.json` / `contracts.json` / `oracle.json` / `bridge.json` / `alts.json`, plus Solana `programs` and per-protocol app deployments.

Install it from GitHub, pinned to a release tag: `npm install github:rome-protocol/rome-registry#v0.2.0`. You read it through getters — `getChain`, `getTokens`, `getContracts`, `getOracle`, `getBridge`, `getAlts`, and `getPrograms(network)` (program ids are keyed by network, not chain id). The package is plain ESM (no bundled TypeScript types today).

Two things to know before you wire it in:

* **`getTokens()` doesn't return `assetRef`.** To find a token's wrapper, match on its **mint** (`mintId`) — e.g. the gas token's wrapper is the entry that shares its mint.
* **It reads JSON from disk (`node:fs`) — it is not browser-safe.** In a web app, project the values you need to a static JSON file at build time and import that in the client. (This is exactly what `create-rome-app` and appia do.)

Contract addresses come back as a list of versions per contract; pick the one whose `status` is `live`. [Reading the contracts list](/reference/contract-addresses#reading-the-contracts-list) shows how. Facts your app doesn't read live also appear in the [Contract Addresses](/reference/contract-addresses) reference.

### rome-sdk-ts — the write path, both lanes

Install from GitHub: `npm install github:rome-protocol/rome-sdk-ts#v0.4.0`.

`@rome-protocol/sdk` (v0.4.0) wraps everything a Rome write needs so you don't hand-roll calldata or fees:

* **`submitRomeTx`** — the EVM-lane write path: sizes gas off `eth_estimateGas` (padded, with a fallback ceiling when estimation reverts) and supplies EIP-1559 fees.
* **`submitRomeTxV1`** — the same, for a **Solana wallet** driving your EVM app: one Solana v1 transaction per call, two wallet prompts. The synthetic sender holds nothing at rest; `buildFundLeg`/`buildSweepLeg` move value in and out as an ERC-20 (`wUSDC`), **not** native `msg.value`; the plain Solana instructions around a call (the fund leg, the wallet token-account creation before a sweep) go through `submitSolanaInstructionsV1` as v1 transactions too. `submitRomeTxSolanaLane` is the earlier path, which builds a legacy Solana transaction in the client and auto-provisions a fresh synthetic (`create_pda`) on first use.
* **PDA/ATA derivation, CPI `invoke`/`invoke_signed` encoders, precompile bindings**, and a **`/bridge`** subpath (`@rome-protocol/sdk/bridge`) — quote-first bridge client.

Full API + examples: [Rome SDK](/products/rome-sdk), and the guides [Call Solana from EVM](/developer-guides/call-solana-from-evm), [Call EVM from Solana](/developer-guides/call-evm-from-solana), and [Build a dual-lane app](/developer-guides/dual-lane-app).

### rome-solidity — the contract-side toolkit

Install it from GitHub, not npm. With npm or Hardhat: `npm install github:rome-protocol/rome-solidity`. With Foundry: `forge install rome-protocol/rome-solidity`, then add the remapping `@rome-protocol/rome-solidity/=lib/rome-solidity/` to `foundry.toml` (under `[profile.default]`, as `remappings = [...]`) so that `@rome-protocol/rome-solidity/contracts/interface.sol` resolves. The package name in `package.json` is `@rome-protocol/rome-solidity`, so the same import path works with both.

What your contracts import:

* **`contracts/interface.sol`** — the precompile interfaces bound to their addresses: **CPI** `ICrossProgramInvocation` (`0xFF…08`), **Helper** `IHelperProgram` (`0xFF…09`, ATA/PDA creation, SPL transfers, gas↔lamports), **Withdraw** `IWithdraw` (`0x42…16`), and **System** (`0xFF…07`). A gas-optimised **cached** family also lives here; a contract uses one track consistently. Full address table: [Contract Addresses](/reference/contract-addresses).
* **SPL/ERC-20 wrappers** — `SPL_ERC20` (CPI-based) and `SPL_ERC20_cached` (the cached track, used on devnet); any SPL mint is already an ERC-20 through these.
* **Oracle adapters** (`contracts/oracle/`) — the Pyth/Switchboard adapters `rome-oracle-gateway` deploys, read via `IAggregatorV3Interface`.
* Worked examples in `contracts/examples/`.

The Solidity SDK section of [Rome SDK](/products/rome-sdk) shows the import patterns and the precompile bindings in code.

## What's next

* [Choose your core](/core-concepts/choose-your-core) — which side holds your logic.
* [Quickstart](/getting-started/quickstart) — deploy your first contract.
* [create-rome-app](https://github.com/rome-protocol/create-rome-app) — scaffold a dual-lane app.
* Each repo's `AGENTS.md` — the by-starting-point route to the closest example.


# Quickstart

Deploy your first Solidity contract on Rome in under 5 minutes.

Deploy your first Solidity contract on Rome in under 5 minutes. This guide targets [**Hadrian**](/networks/hadrian), the public testnet chain. [Rubicon](/networks/rubicon) is the mainnet chain, where gas is real USDC; try everything on a test chain first.

## Prerequisites

* [Node.js](https://nodejs.org/) v22.13+
* [MetaMask](https://metamask.io/) browser extension
* A Solana wallet set to **Devnet**, only if you fund from Solana (step 2). Calling contracts from a Solana wallet needs one that signs Solana v1 transactions, which is covered in [Open your contract to Solana users](/developer-guides/open-to-solana-users).

## 1. Add Hadrian to MetaMask

Add it manually (Settings → Networks → Add network):

| Field           | Value                                           |
| --------------- | ----------------------------------------------- |
| Network Name    | Rome Hadrian                                    |
| RPC URL         | `https://hadrian.testnet.romeprotocol.xyz/`     |
| Chain ID        | `200010`                                        |
| Currency Symbol | `USDC`                                          |
| Block Explorer  | `https://via-hadrian.testnet.romeprotocol.xyz/` |

## 2. Fund your wallet

Gas is paid in the chain's gas token, which is USDC on Hadrian. On Hadrian it is backed by Circle's Solana Devnet USDC (mint `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`), so devnet SOL is not gas. There is no Rome faucet. Two ways to get USDC onto Hadrian:

**From Solana** — get Solana Devnet USDC from the [Circle faucet](https://faucet.circle.com/) (choose Solana Devnet) and a little devnet SOL for Solana fees from the [Solana faucet](https://faucet.solana.com/), with your Solana wallet set to Devnet. Then use the [Rome Bridge](/apps-on-rome/bridge-api) with Hadrian as the destination and its Solana route, with your EVM wallet's address as the recipient; the bridge asks that EVM wallet to sign a claim transaction on Hadrian, which turns the USDC into gas.

**By bridging in from an EVM chain** — mint test USDC on Ethereum Sepolia from the [Circle faucet](https://faucet.circle.com/), then bring it to Rome with the [Rome Bridge](/apps-on-rome/bridge-api) (CCTP). You'll need a little Sepolia ETH for the source-side transaction — see [Faucets](/resources/faucets) for every faucet.

**How much?** A write on Rome costs more than Ethereum intuition suggests, because most of its gas pays for creating Solana accounts: a recipient that has never been used, a new token account, a deployed contract. Use `eth_estimateGas`, which standard tooling does, and never hardcode 21,000. The estimate includes the account creation, Rome charges the gas a transaction actually uses (in measurements on Hadrian that was never more than the estimate), and the SDK's `submitRomeTx` pads the estimate by 1.3x as a margin, which costs nothing because only the gas used is charged. Fund at least **0.5 USDC of gas**; 1 USDC leaves comfortable headroom. Numbers in [JSON-RPC Support](/reference/json-rpc) show what individual writes cost.

If a Solana-side deposit fails with an "insufficient SOL" warning despite a funded wallet, set your Solana wallet to Testnet Mode / Devnet (see the [FAQ](/resources/faq)).

## 3. Create a Hardhat project

```bash
mkdir rome-hello && cd rome-hello
npx hardhat --init
```

Accept the defaults when prompted — Hardhat 3, the current directory, and the TypeScript + Node Test Runner + viem template — and let it install dependencies. The template ships an example `Counter` contract with tests; they don't interfere with this guide.

## 4. Configure Hardhat for Rome

Edit the generated `hardhat.config.ts` and add the Rome networks inside `defineConfig({ ... })`:

```typescript
  networks: {
    hadrian: {
      type: "http",
      chainType: "l1",
      chainId: 200010,
      url: "https://hadrian.testnet.romeprotocol.xyz/",
      accounts: [configVariable("PRIVATE_KEY")],
    },
  },
```

`configVariable("PRIVATE_KEY")` resolves from the environment, so export your MetaMask private key:

```bash
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
```

(For anything beyond a throwaway key, prefer the encrypted keystore that ships with the toolbox: `npx hardhat keystore set PRIVATE_KEY`.)

## 5. Write a contract

Create `contracts/HelloRome.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract HelloRome {
    string public greeting = "Hello from Solana!";
    uint256 public counter;

    event Greeted(address indexed sender, uint256 count);

    function greet() external returns (string memory) {
        counter++;
        emit Greeted(msg.sender, counter);
        return greeting;
    }

    function setGreeting(string calldata _greeting) external {
        greeting = _greeting;
    }
}
```

## 6. Deploy

Create `scripts/deploy.ts`:

```typescript
import hardhat from "hardhat";

async function main() {
  const { viem } = await hardhat.network.connect();
  const publicClient = await viem.getPublicClient();

  const hello = await viem.deployContract("HelloRome");
  console.log("HelloRome deployed to:", hello.address);

  const hash = await hello.write.greet();
  await publicClient.waitForTransactionReceipt({ hash });

  console.log("Counter:", (await hello.read.counter()).toString());
  console.log("Greeting:", await hello.read.greeting());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Deploy to Hadrian:

```bash
npx hardhat run scripts/deploy.ts --network hadrian
```

Expected output (your address will differ):

```
HelloRome deployed to: 0x09c437e305eeb698e1776cff23ca16c1bb25aabd
Counter: 1
Greeting: Hello from Solana!
```

Open the deployed address in the [explorer](https://via-hadrian.testnet.romeprotocol.xyz/) to see the Solana instructions behind your transaction. Your Solidity contract is now running on Solana.

## What's Next

* [Deploy Solidity](/developer-guides/deploy-solidity) — Hardhat and Foundry deployment in depth
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — call Solana programs from Solidity via CPI
* [Architecture](/getting-started/architecture) — how Rome executes EVM inside Solana

## Common Errors

| Error                | Cause                                                      | Fix                                                                                         |
| -------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `insufficient funds` | Not enough gas token for the gas limit times the gas price | Fund at least 0.5 USDC of gas (see step 2) with the [Rome Bridge](/apps-on-rome/bridge-api) |
| `nonce too low`      | Transaction nonce mismatch                                 | Reset the account (MetaMask → Settings → Advanced → Clear activity)                         |
| `execution reverted` | Contract execution failed                                  | Check contract logic; use `eth_call` to debug                                               |
| Connection timeout   | RPC unreachable                                            | Verify the RPC URL                                                                          |


# Key Concepts

Essential Solana and Rome EVM terminology for developers — programs, accounts, PDAs, CPI, SPL tokens, and atomic vs. iterative execution.

Essential terminology and concepts for building on Rome Protocol.

## Solana Concepts

**Program** — Solana's equivalent of a smart contract. Programs are stateless executables deployed on-chain. The Rome EVM is itself a Solana program.

**Account** — All state on Solana lives in accounts. Each account has an owner (program), a balance (lamports), and data. Unlike Ethereum, code and state are stored in separate accounts.

**PDA (Program Derived Address)** — A deterministic address derived from seeds and a program ID. PDAs allow programs to "own" accounts without a private key. Rome uses PDAs to map Ethereum addresses to Solana accounts.

**CPI (Cross-Program Invocation)** — One Solana program calling another within the same transaction. This is how Rome EVM contracts interact with Jupiter, Kamino, SPL Token, and other Solana programs.

**SPL Token** — Solana's standard token program. Equivalent to ERC-20 on Ethereum. All fungible tokens on Solana (USDC, SOL, etc.) are SPL tokens.

**Token-2022** — The next-generation SPL token program with extensions like Transfer Hooks, Confidential Transfers, and Permanent Delegates.

**Transfer Hook** — A Token-2022 extension that invokes a program on every `transfer_checked` call.

**ATA (Associated Token Account)** — A deterministic token account for a given wallet + mint pair. Every user has one ATA per token they hold.

**Lamports** — The smallest unit of SOL. 1 SOL = 1,000,000,000 lamports (10^9).

**Compute Units (CU)** — Solana's equivalent of Ethereum gas. Each transaction has a compute budget (default \~200K CU, max \~1.4M CU). Operations consume CU.

## Rome Concepts

**Rome EVM Program** — The Solana program that contains the EVM bytecode interpreter. Deployed at a specific program ID per environment.

**Chain ID** — Each application on Rome gets its own EVM chain ID. This creates isolated EVM environments that share the same underlying Solana state.

**Atomic Execution (VmAt)** — An EVM transaction that executes entirely within a single Solana transaction. Used for most operations.

**Iterative Execution (VmIt)** — An EVM transaction split across multiple Solana transactions, each step packing as many opcodes as fit in one Solana transaction's compute budget (adaptive, not a fixed count). Used for compute-intensive operations like BN254 pairing.

**Holder Account** — An on-chain buffer that stores large EVM transactions (up to 80 KB) that exceed the 4 KB Solana (v1) transaction size limit. Managed transparently by the SDK.

**StateHolder** — An on-chain account that stores serialized VM state between iterative execution steps.

**Rome Proxy** — The JSON-RPC server (port 9090) that translates Ethereum API calls into Solana transactions. Your MetaMask and Hardhat connect here.

**Hercules** — The block indexer that monitors Rome EVM events on Solana and produces Ethereum-compatible block data.

**Payer** — A Solana keypair that signs and pays for Solana transactions on behalf of EVM users. Managed by the Proxy via payer pools.

## Token Concepts

**SPL\_ERC20 / SPL\_ERC20\_cached** — ERC-20 wrapper contracts representing an SPL token inside Rome EVM. For a wallet, the wrapper reads the balance straight from the underlying SPL token account. Tokens held by a contract sit in the wrapper's own escrow token account and are tracked in the wrapper's storage (see [Integrate a Solana program](/developer-guides/integrate-solana-program)). The factory deploys the cached variant today.

**ERC20SPLFactory** — A factory contract that deploys wrappers for any SPL token.

**Registry** — Canonical wrappers, gas tokens, and bridge wiring are curated off-chain in the [rome-protocol/registry](https://github.com/rome-protocol/rome-registry); there is no on-chain token-registry contract. This keeps each asset mapped to a single canonical SPL mint.

## Precompiles

**Standard Ethereum Precompiles** — ecrecover (0x01), SHA-256 (0x02), RIPEMD-160 (0x03), identity (0x04), modexp (0x05), BN254 ecAdd/ecMul/ecPairing (0x06-0x08), Blake2f (0x09).

**System Precompile** (`0xFF...07`) — PDA derivation and base58 conversion from Solidity.

**CpiProgram Precompile** (`0xFF...08`) — Cross-program invocation (`invoke` / `invoke_signed`) plus cross-state read shortcuts.

**HelperProgram Precompile** (`0xFF...09`) — ATA/PDA creation, SPL transfers, and gas↔lamports conversion; the primary surface for user-PDA-signed SPL operations.

**Withdraw Precompile** (`0x42...16`) — Withdraw SOL or SPL tokens from EVM back to Solana.

A cached-track family (`0xff…04/05/06/0b`) mirrors these for CU-efficient reads; a contract uses one track consistently.

## Transaction Types

**RheaTx** — A single EVM transaction on one rollup. The most common type.

**RemusTx** — Multiple EVM transactions across different rollups, executed atomically. If any transaction fails, all revert.

**RomulusTx** — Combined EVM transactions + native Solana instructions in one atomic operation. The most powerful type — mix Solidity and Solana in a single transaction.

## What's Next

* [Execution Model](/core-concepts/execution-model) — deep dive into how EVM transactions execute on Solana
* [Token Interop](/core-concepts/token-interop) — how ERC-20 and SPL tokens interact
* [Constraints](/core-concepts/constraints) — important limits and boundaries


# Overview

Public Rome chains — chain IDs, RPC endpoints, explorers, and gas tokens.

Rome's public EVM chains for builders are **Rubicon** (mainnet) and **Hadrian** (testnet), each executing inside a Solana program. Hadrian settles on Solana Devnet, so its USDC and SOL are not real money. Rubicon settles on Solana mainnet and its gas is real USDC. Build and test on Hadrian first.

| Network                                | Chain ID | RPC                                         | Explorer                                                     | Gas token   |
| -------------------------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------ | ----------- |
| [Rubicon](/networks/rubicon) (mainnet) | 7531     | `https://rubicon.mainnet.romeprotocol.xyz/` | [via-rubicon](https://via-rubicon.mainnet.romeprotocol.xyz/) | USDC (real) |
| [Hadrian](/networks/hadrian) (testnet) | 200010   | `https://hadrian.testnet.romeprotocol.xyz/` | [via-hadrian](https://via-hadrian.testnet.romeprotocol.xyz/) | USDC (test) |

Chain metadata — chain IDs, contract addresses, token mints, gas pools — is canonical in the [Rome registry](https://github.com/rome-protocol/rome-registry). Don't hardcode addresses; read them from there. Each contract lists its versions, and the one to use has `status` `live` — see [Reading the contracts list](/reference/contract-addresses#reading-the-contracts-list). The registry covers Hadrian. Rubicon is not in it yet, nor in `create-rome-app` or `rome-cli`, so take Rubicon's values from [its page](/networks/rubicon).

Operators run their own Rome ZK EVM chains on Solana devnet. Each chain posts its batches to Rome's shared inbox program and settles its proved roots through the shared settlement program. See [Run a rollup](/rome-zk-evm/run-a-rollup) to start one.

## Add a network to your wallet

Each network page lists the manual MetaMask values. WebSocket subscriptions are served on the same host as the HTTPS RPC (swap `https://` for `wss://`).

## Fund a wallet

Gas on every Rome chain is USDC, and there is no Rome faucet. On Hadrian, bring in test USDC with the [Rome Bridge](/apps-on-rome/bridge-api), either from a testnet such as Ethereum Sepolia or from Solana Devnet. A wallet that works from the Solana side also needs a little devnet SOL for Solana fees. On Rubicon the USDC is real; bridge it in with the Rome Bridge on mainnet. Where to get each token is in [Faucets](/resources/faucets).

## Local development

There is no public local stack. Develop against a public chain — see the [Quickstart](/getting-started/quickstart).


# Hadrian (testnet)

Rome Hadrian — the public testnet chain (chain ID 200010).

Hadrian is Rome's public testnet chain and the canonical benchmark chain — the reference apps (Uniswap V3/V4, Compound, Aave) are validated here first. It's an EVM chain executing inside a Solana program and settling on Solana Devnet.

## Network details

| Field            | Value                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------- |
| Chain ID         | `200010`                                                                              |
| RPC URL          | `https://hadrian.testnet.romeprotocol.xyz/`                                           |
| WebSocket        | `wss://hadrian.testnet.romeprotocol.xyz/`                                             |
| Explorer         | [via-hadrian.testnet.romeprotocol.xyz](https://via-hadrian.testnet.romeprotocol.xyz/) |
| Gas token        | USDC (18 decimals on the EVM side)                                                    |
| Solana USDC mint | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU` (Circle's devnet USDC, 6 decimals)     |
| Rome program ID  | `RPTWwELXAY4KC9ZPHhaxp7Sq1hHtU3HNEgLbSegCcWf`                                         |

## Add to MetaMask

Enter the values above manually (Settings → Networks → Add network).

## Fund a wallet

Gas is USDC, so you need USDC on Hadrian. There is no Rome faucet.

1. Get test USDC. From a Solana wallet, take Solana Devnet USDC (mint above) from [Circle's faucet](https://faucet.circle.com/) and a little devnet SOL for Solana fees from the [Solana faucet](https://faucet.solana.com/), with your wallet set to Devnet. From an EVM testnet such as Ethereum Sepolia, mint test USDC from the same Circle faucet. [Faucets](/resources/faucets) lists every source.
2. Bring it in with the [Rome Bridge](/apps-on-rome/bridge-api) (choose Hadrian as the destination). A bridged USDC deposit lands as gas.

If a Solana-side transaction fails with an "insufficient SOL" warning despite a funded wallet, set your Solana wallet to Testnet Mode / Devnet (see the [FAQ](/resources/faq)).

## Contracts

Deployed contract addresses for Hadrian live in the [Rome registry](https://github.com/rome-protocol/rome-registry/tree/main/chains/200010-hadrian) and are summarized on the [Contract Addresses](/reference/contract-addresses) page.


# Rubicon (mainnet)

Rome Rubicon — the public mainnet chain (chain ID 7531). Settles on Solana mainnet, gas is real USDC.

Rubicon is Rome's public mainnet chain. It is an EVM chain that executes inside the Rome program on Solana mainnet and settles there. Gas is paid in USDC, and that USDC is real money. There is no faucet.

Rubicon is not in the public [Rome registry](https://github.com/rome-protocol/rome-registry), in `create-rome-app` or in `rome-cli` yet. Tools that read chain values from the registry cannot target it. Fill the values by hand from this page.

## Network details

| Field                 | Value                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------ |
| Chain ID              | `7531`                                                                                                 |
| RPC URL               | `https://rubicon.mainnet.romeprotocol.xyz/`                                                            |
| WebSocket             | `wss://rubicon.mainnet.romeprotocol.xyz/`                                                              |
| Explorer              | [via-rubicon.mainnet.romeprotocol.xyz](https://via-rubicon.mainnet.romeprotocol.xyz/)                  |
| Contract verification | `https://verify.mainnet.romeprotocol.xyz` (see [Verify Contracts](/developer-guides/verify-contracts)) |
| Settles on            | Solana mainnet                                                                                         |
| Rome program ID       | `RomePq9X3iAoTr813HR7uRarafFDUJty6GdiEceYpzX`                                                          |
| Gas token             | USDC, 18 decimals on the EVM side                                                                      |
| Solana USDC mint      | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` (6 decimals)                                            |

Add it to a wallet by entering the values above manually (Settings → Networks → Add network), with the currency symbol `USDC`. WebSocket subscriptions are served on the same host as the HTTPS RPC.

## Gas

Gas is USDC. The price is read the usual way, with `eth_gasPrice`. Rubicon's price is higher than on Hadrian and it varies, so do not copy a number from this page. As a comparison, a read of `eth_gasPrice` on Rubicon returned about 104.6 gwei while Hadrian returned about 10.4 gwei, roughly ten times less. The quickstart contract, built without the optimizer as `npx hardhat run` builds it, estimates about 14 million gas on Rubicon, so a first deploy costs about 1.5 USDC at that price (about 9.7 million gas and 1 USDC with the optimizer on).

Use `eth_estimateGas` for every write and never hardcode 21,000. Most of a write's cost is creating Solana accounts (a never-used recipient, a new token account, a deployed contract), and the estimate includes it. For calls into Solana programs, add a margin, as the SDK's 1.3x pad does, and set a gas ceiling yourself if the estimate itself reverts. See [JSON-RPC Support](/reference/json-rpc) for the measured numbers.

## Fund a wallet

Bring USDC in with the [Rome Bridge on mainnet](https://bridge-api.mainnet.romeprotocol.xyz/). At the time of writing its route list for Rubicon has USDC over CCTP with Ethereum, Base, Arbitrum, Polygon, Avalanche, Monad and OP, and SOL and USDC from a Solana wallet. There is no ETH route. The live list is at [`/v1/routes`](https://bridge-api.mainnet.romeprotocol.xyz/v1/routes). By default, USDC that arrives through the bridge lands as gas. The bridge page can deliver it as the wUSDC token instead. Details of the bridge are on the [Rome Bridge](/apps-on-rome/bridge-api) page, and [Faucets](/resources/faucets) says what applies on mainnet.

## Contracts and wrappers

Rubicon's wrapper addresses are listed in [Per-network wrapper addresses](https://docs.rome.builders/networks/pages/QQmOT5sDIQcUnRUM4fU6#id-4.-per-network-wrapper-addresses) and on [Contract Addresses](/reference/contract-addresses). They are not copied here so that there is one place to keep current.

## Calling Rubicon from a Solana wallet

The Solana lane (a Solana wallet calling your EVM contract) needs two things on mainnet.

* **A Solana mainnet RPC.** The SDK sends the signed Solana transaction to the Solana RPC you give it, not to the Rome proxy. Use an RPC for Solana mainnet from a provider you have an account with.
* **A wallet that signs Solana v1 transactions.** Every Solana-lane call is a v1 transaction. Check that your wallet signs them before you build on it. On Hadrian, Solflare signs them and Phantom does not yet. Whether a given wallet signs them against Rubicon is not covered here, so test yours first.

Because the registry has no Rubicon entry, fill the SDK's `deps` by hand with the values from the table above. The field-by-field map is in [Open your contract to Solana users](https://docs.rome.builders/networks/pages/QQmOT5sDIQcUnRUM4fU6#id-3.-let-a-solana-wallet-call-it).

## Before you deploy

* Develop and test on [Hadrian](/networks/hadrian). Rubicon runs the same EVM, so the same artifacts deploy, with the chain ID and the wrapper addresses swapped.
* Every transaction on Rubicon spends real USDC. Run the full round trip once with a small amount before you announce anything.
* A user who moves wrapper tokens needs the one-time wrapper grant described in [Token Interop](/core-concepts/token-interop#one-time-grant-per-wallet), on Rubicon as on every other chain.


# Overview

Live applications running on Rome — DeFi, explorers, bridges, and portals.

Rome is live software, not just a spec. These apps run on the public chains today and each one demonstrates a different way EVM and Solana meet on Rome — one shared state, no wrapped venue, no bridge between the two sides.

Links open each app on Hadrian, Rome's public testnet, unless marked mainnet; Privacy DEX runs on Solana Devnet directly.

## DeFi

| App                                          | What it is                                                                                                                        | Links                                                 | Source                                                                                                                  |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Rome DEX**                                 | One native Solana pool where Solana-address and EVM-address users trade and LP the same reserves — one market, two identities.    | [testnet](https://dex.devnet.romeprotocol.xyz/)       | [rome-dex](https://github.com/rome-protocol/rome-dex)                                                                   |
| **Aerarium**                                 | Compound v3 with Solana and EVM identities in one pool — supply and borrow from either side of the same market.                   | [testnet](https://aerarium.devnet.romeprotocol.xyz/)  | [aerarium](https://github.com/rome-protocol/aerarium)                                                                   |
| **Aave v3**                                  | Aave v3 deployed on Rome with live reserves — the same hardened Solidity, opened to both sides.                                   | [testnet](https://aave.testnet.romeprotocol.xyz/)     | [demo](https://github.com/rome-protocol/rome-aave-v3-demo) · [contracts](https://github.com/rome-protocol/rome-aave-v3) |
| **Cardo**                                    | Solana DeFi from an EVM account — Jupiter perps, Meteora swaps, Marinade staking, and more, with atomic multi-dapp Compose flows. | [testnet](https://cardo.devnet.romeprotocol.xyz/)     | [cardo](https://github.com/rome-protocol/cardo)                                                                         |
| **Appia**                                    | Solana positions for users of other chains — enter from an EVM chain, manage on Solana, exit home. *In validation.*               | [testnet](https://appia.devnet.romeprotocol.xyz/)     | [appia](https://github.com/rome-protocol/appia)                                                                         |
| [**Privacy DEX**](/apps-on-rome/privacy-dex) | Swaps, a USDC vault, and limit orders on Solana using Helius Privacy private balances and Rome's escrow program.                  | [devnet](https://privacy-dex.devnet.romeprotocol.xyz) | —                                                                                                                       |

## Infrastructure

| App                                                | What it is                                                                                                                                                                                 | Links                                                                                                                                    | Source                                                                      |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Rome App**                                       | The day-to-day utility surface — wrap, swap, LP, and bridge across Rome chains.                                                                                                            | [testnet](https://app.devnet.romeprotocol.xyz/)                                                                                          | —                                                                           |
| [**Rome Bridge**](/apps-on-rome/bridge-api)        | A bridge between Rome, seven EVM chains and Solana on CCTP v2 and Solana-native rails (Wormhole on the testnet), plus a public API any app integrates to embed bridging — no custody keys. | [mainnet](https://bridge-api.mainnet.romeprotocol.xyz/?chain=7531) · [testnet](https://bridge-api.devnet.romeprotocol.xyz/?chain=200010) | [rome-bridge-api](https://github.com/rome-protocol/rome-bridge-api)         |
| [**Via Explorer**](/apps-on-rome/via)              | The cross-VM explorer — every EVM transaction that touches Solana shows the exact Solana program and instruction behind it.                                                                | [Hadrian](https://via-hadrian.testnet.romeprotocol.xyz/)                                                                                 | —                                                                           |
| [**Oracle Gateway**](/apps-on-rome/oracle-gateway) | Pyth and Switchboard price feeds behind Chainlink-compatible interfaces, with keeper-guaranteed freshness.                                                                                 | [portal](https://oracle.testnet.romeprotocol.xyz/?chain=hadrian)                                                                         | [rome-oracle-gateway](https://github.com/rome-protocol/rome-oracle-gateway) |
| [**Bloom**](/apps-on-rome/bloom)                   | Permissioned real-world assets — issue a Solidity asset once, then hold and buy it from EVM and Solana wallets alike, with the allowlist enforced on the token itself.                     | [testnet](https://bloom.devnet.romeprotocol.xyz/)                                                                                        | —                                                                           |

## Build one

Building on Rome? Start with the [Quickstart](/getting-started/quickstart), then [Deploy Solidity](/developer-guides/deploy-solidity). To launch your own chain, see [App Sovereignty](/products/app-sovereignty).


# Rome Bridge

Rome Bridge — a standalone bridge and an integrable API, with no custody keys.

Rome Bridge is two things: a **standalone bridge** between Rome and seven EVM chains plus Solana, and a **Bridge API** any app integrates to embed bridging in its own UX. It moves assets over CCTP v2 (USDC), Solana-native rails (SOL and USDC), and, on the testnet, Wormhole (ETH into Rome).

It holds **no custody keys** — settlement always requires the user's own signature, never the service's. The API orchestrates and can sponsor fees; the real bridge is on-chain.

## Supported chains and assets

The bridge runs on Rubicon (mainnet) and on Hadrian (the public testnet). Each has its own bridge page and API host. The expected time for a transfer is in the quote.

On both networks, besides SOL and USDC, `POST /v1/quote` with `asset: "SPL"` and `splAsset: {mint, decimals}` returns the transfer for any SPL token between a Solana wallet and Rome, and your wallet signs the transactions it returns. The token arrives on Rome as its wrapper. This route is not listed in `/v1/routes`.

### Rubicon (mainnet)

| Asset             | Rail    | Direction                 | Chains                                                  |
| ----------------- | ------- | ------------------------- | ------------------------------------------------------- |
| **USDC**          | CCTP v2 | Into Rome and out of Rome | Ethereum, Base, Arbitrum, Polygon, Avalanche, Monad, OP |
| **SOL**, **USDC** | Solana  | Into Rome and out of Rome | A Solana wallet                                         |

There is no ETH route on mainnet. MetaMask's gas-free mode works for CCTP deposits. When MetaMask's relayer submits your deposit, the bridge accepts it if the depositor is the sender named in the quote and the burn matches the quote. A relayed transaction may contain only one burn.

Moving USDC out of Rome with CCTP spends the wUSDC token, not the gas USDC, and the transfer ends with a claim transaction on the destination chain. The bridge page's Wrap tab turns gas into wUSDC in one signature.

The list of routes is [`/v1/routes`](https://bridge-api.mainnet.romeprotocol.xyz/v1/routes). [`/v1/chains`](https://bridge-api.mainnet.romeprotocol.xyz/v1/chains) lists the Rome chain with its registry assets and capabilities. On mainnet that list still names Wormhole and ETH, although no ETH route exists.

### Hadrian (testnet)

| Asset             | Rail     | Direction                 | Chains                                                                                                    |
| ----------------- | -------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| **USDC**          | CCTP v2  | Into Rome and out of Rome | Ethereum Sepolia, Arbitrum Sepolia, Base Sepolia, Polygon Amoy, Avalanche Fuji, Monad Testnet, OP Sepolia |
| **ETH**           | Wormhole | Into Rome                 | Ethereum Sepolia                                                                                          |
| **SOL**, **USDC** | Solana   | Into Rome and out of Rome | A Solana wallet                                                                                           |

The live set is always in [`/v1/routes`](https://bridge-api.devnet.romeprotocol.xyz/v1/routes) and [`/v1/chains`](https://bridge-api.devnet.romeprotocol.xyz/v1/chains). Need test tokens? See [Faucets](/resources/faucets).

## Use it standalone

Open the bridge UI for the network you want:

* Rubicon (mainnet): [bridge-api.mainnet.romeprotocol.xyz](https://bridge-api.mainnet.romeprotocol.xyz/?chain=7531)
* Hadrian (testnet): [bridge-api.devnet.romeprotocol.xyz](https://bridge-api.devnet.romeprotocol.xyz/?chain=200010)

The `?chain=` parameter selects the Rome chain.

## Integrate the API

The API is public. List supported chains and rails:

```bash
curl https://bridge-api.mainnet.romeprotocol.xyz/v1/chains   # Rubicon
curl https://bridge-api.devnet.romeprotocol.xyz/v1/chains    # Hadrian
```

`POST /v1/quote` takes the non-Rome side of the transfer in `sourceChain`, in both directions: CAIP-2 (`eip155:8453`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) or the older names `ethereum` / `solana`, which are still accepted with a `Sunset` header (use CAIP-2). `GET /v1/routes` takes no parameters and lists the transfers that are available. A quote request:

```bash
curl -X POST https://bridge-api.devnet.romeprotocol.xyz/v1/quote \
  -H 'content-type: application/json' \
  -d '{"asset":"USDC","direction":"to-rome","sourceChain":"eip155:84532","romeChainId":"200010","amount":"1000000","sender":{"ethereum":"<your 0x address>"},"recipient":"<recipient on Rome>"}'
```

`amount` is a string of base units. USDC has 6 decimals, so `"1000000"` is 1 USDC. The response carries the expected time in `etaSeconds`. See the endpoint responses for the rail set and the other fields.

## How settlement works

The off-chain API is an orchestrator and fee sponsor only. Inbound transfers settle on-chain trustlessly via a user-signed authorization; outbound withdrawals go through the on-chain bridge contract. No step debits user funds without the user's signature.


# Oracle Gateway portal

The Oracle Gateway portal — register price feeds and configure consumers per chain.

The Oracle Gateway portal is the customer-facing surface for Rome's [Oracle Gateway](/products/oracle-gateway) — the system that exposes Pyth and Switchboard price feeds behind Chainlink-compatible interfaces, with a Rome-run keeper refreshing the cached and book feeds.

Use the portal to browse registered feeds and configure consumers per chain: [oracle.testnet.romeprotocol.xyz](https://oracle.testnet.romeprotocol.xyz/).

## What it covers

* **Registered feeds** — the price feeds live on each chain, with their sources and freshness windows.
* **Consumer configuration** — wiring a contract to read a feed through the Chainlink `AggregatorV3Interface`.

For the interface details, adapter architecture, how the keeper refreshes feeds and which feeds answer today, see the [Oracle Gateway product page](/products/oracle-gateway). Deployed adapter addresses are canonical in the [registry](https://github.com/rome-protocol/rome-registry).


# Via Explorer

Via — the cross-VM block explorer for Rome chains.

Via is Rome's block explorer, built for the cross-VM model. Alongside the usual blocks, transactions, tokens, and addresses, it shows the **Solana side** of every EVM transaction — the exact Solana program and instruction each call resolved to.

Because a Rome EVM transaction executes inside a Solana program, "what actually happened on Solana" is the part a normal EVM explorer can't show. Via does.

## Instances

Via runs per chain:

* Hadrian (testnet): [via-hadrian.testnet.romeprotocol.xyz](https://via-hadrian.testnet.romeprotocol.xyz/)

## What to look for

Open any transaction and read the Solana instructions behind it — CPI calls into SPL Token, Meteora, or any Solana program, plus the compute units consumed. This is the reliable way to see real Solana cost (EVM `gasUsed` is not a faithful proxy for Solana compute units on Rome).


# Bloom — RWA issuance

Bloom — issue and hold permissioned real-world assets on Rome, reachable from both EVM and Solana wallets.

Bloom issues and holds **permissioned real-world assets** on Rome. An asset is an ordinary Solidity contract, deployed once, whose rule for *who may hold it* lives inside the contract and is enforced on every transfer. The same asset is reachable from an EVM wallet and from a Solana wallet — one deployment, both wallet worlds, with no bridge and no second token.

This documentation is written per audience, because the audiences do not overlap: an issuer never buys, a Solana holder never issues, and a builder needs the contract surface none of the others touch.

## The guides

* [**Investing**](/apps-on-rome/bloom/investing) — for a buyer. Browse an asset, check eligibility, apply for the allowlist, buy, and hold — including the one thing that is genuinely different for a holder who signs with a Solana wallet.
* [**Getting funds to Rome**](/apps-on-rome/bloom/funding) — the cash-in before the buy. Bridge USDC or ETH in from Sepolia, then wrap the bridged gas into the wUSDC every sale is priced in.
* [**Issuing**](/apps-on-rome/bloom/issuing) — for an issuer. The console, the creation wizard, the per-asset console sign-in gate, and each of the six administration tabs.
* [**Building on Bloom**](/apps-on-rome/bloom/building) — for a developer. A ten-step method to take a Solidity asset onto Rome as a permissioned token, open it to both wallet worlds, and plug in your own KYC — with the real contract surface and code.

The screenshots throughout are not mock-ups. Two evidence pages trace them to real transactions on chain:

* [**Evidence — an issuer**](/apps-on-rome/bloom/evidence) — the demo asset issued and administered: the piecewise deploy, the sale, the batch allowlist, the roles.
* [**Evidence — an investor**](/apps-on-rome/bloom/evidence-investor) — the same asset bought twice, once from an EVM wallet and once from a Solana wallet, with no bridge.

## Trade and Credit

Bloom's landing page opens four apps: Invest and Issue, which the guides above cover, and two more that work on the same permissioned assets.

**Trade.** Swap and provide liquidity in pools of an approved asset against wUSDC. The swap card takes the amount you pay, shows what you receive, the slippage limit and the pool's fee (0.30% on the live pool), and swaps through the pool. The Pools tab lists each pool with its access (permissioned pools admit only approved holders; permissionless pools are open), its price, liquidity and fee, and lets you add a new pool. A Solana wallet and an EVM wallet both work, with the prompts described in [LANES.md](/apps-on-rome/bloom/lanes).

**Credit.** A permissioned lending market built on Morpho Blue. In Vaults you deposit wUSDC to earn and withdraw it later. In Borrow you pick a market, supply an approved asset as collateral, borrow against it, repay, or lend to the market directly. Portfolio shows your positions. Borrowing and lending carry risk, including liquidation and loss of principal, and Credit carries that notice on its screens; it is not financial advice. The larger Credit actions are the ones a Solana wallet may have relayed (see [LANES.md](/apps-on-rome/bloom/lanes#calls-too-large-for-one-transaction)).

Both apps are live on devnet and open from the Bloom landing page. Neither has its own guide yet.

## What "permissioned" means here

> The rule travels with the asset. Who may hold it is enforced by the asset contract itself, on every transfer — not by this website, not by a bridge, and not by a sequencer filter. The chain holds one fact per address: a single yes-or-no. No documents and no personal data ever touch this app or the chain.

The compliance model in full lives in [COMPLIANCE.md](/apps-on-rome/bloom/compliance); the two-lane (EVM and Solana) mechanics your holders use are in [LANES.md](/apps-on-rome/bloom/lanes). These guides link to them rather than restating them.


# Investing

Bloom is a marketplace for **permissioned real-world assets**. Each asset is an ordinary Solidity contract deployed once on a Rome chain, and the rule for who may hold it lives inside that contract. This guide walks a buyer from the landing page through browsing an asset, checking eligibility, applying for the allowlist, buying, and holding — and then covers the one thing that is genuinely different for a holder who signs with a Solana wallet.

> **The rule travels with the asset.** Who may hold an asset is enforced by the asset contract itself, on every transfer — not by this website. Nothing on any screen here can override it, and there is no path around it. Bloom is a window onto the chain; the chain is what decides.

***

## What "permissioned" means for a buyer

A permissioned asset has an **allowlist**: a set of addresses the issuer has decided may hold it. The allowlist is not a Bloom feature — it lives on a module attached to the asset contract, and the asset consults it on every mint, burn and transfer.

> **The chain holds one fact about you: a single yes-or-no against your address.** No documents, no personal information and no identity claims are ever stored on the chain or in this app. When an issuer admits you, all that reaches the chain is `isWhitelisted(yourAddress) = true`. The full compliance model is in [COMPLIANCE.md](/apps-on-rome/bloom/compliance).

Two consequences follow, and both are visible in the product:

* Before you can buy, the asset is asked whether **your address** may hold it. A refusal is a fact about the asset's rule, not a problem with your wallet.
* A transfer to an address the issuer has not approved **reverts on chain**. The asset refuses it; no screen has to.

***

## The two wallet worlds

A single Bloom asset is reachable from **both** an EVM wallet (such as MetaMask) and a Solana wallet (such as Solflare). You connect one wallet at a time. A Solana wallet has to be able to sign Solana version 1 transactions: Solflare does on devnet today, and Phantom does not yet.

| Wallet            | Where it holds the asset                               | What signs                                                                                                                               |
| ----------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **EVM wallet**    | Its own EVM address, as on any EVM chain               | An EVM transaction                                                                                                                       |
| **Solana wallet** | An EVM address **derived from your Solana public key** | Your Solana signature, asked for twice per step (see [Paying, and the extra signature prompts](#paying-and-the-extra-signature-prompts)) |

For an EVM wallet there is nothing new to learn. For a Solana wallet the asset sits at a *derived* address — computed as `keccak256(solana_pubkey)[12..32]` — and only your Solana signature can drive it. There is **no bridge, no second token and no EVM account** involved. This is covered in full in [the Solana holder section](#for-a-solana-holder) below and in [LANES.md](/apps-on-rome/bloom/lanes).

***

## The landing page

![The Bloom landing page: the headline, the two wallet-world rows, and the Investor and Issuer doors.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-d24cbcc96b6ec85d6a62caa993ccd38d3aff82fc%2Flanding.png?alt=media)

The landing page (`/`) states what Bloom is — *Solidity assets, open to Solana as well as EVM* — and gives you two doors: **Investor** (which takes you to the open assets at `/invest`) and **Issuer**. As a buyer, choose **Investor**.

Two rows, **Solana wallets** and **EVM wallets**, list the wallets your browser has actually announced through the EIP-6963 and Wallet Standard discovery protocols. Nothing is shown that you do not have installed; each wallet's name and icon are its own.

At the foot, a **Contracts** section lists the addresses this deployment talks to — the token factory, the shared implementation, the permission router, the primary-sale storefront and the cash token — each linking to the block explorer, so the app never asserts an address in its own voice.

### "How is it possible with Solana?"

![The custody explainer: one signature enters on the left and the asset admits it on the right, with no bridge between.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-18d251b822b78890123e6049df399d603523af5d%2Fcustody.png?alt=media)

The landing page carries an explainer that answers how a Solana wallet can hold a Solidity asset. It makes three points:

* **One deployment.** The asset is an ordinary Solidity contract, deployed once. There is no bridged copy of it anywhere — a second copy is exactly what would let it escape the rules.
* **Two wallet worlds.** An EVM wallet reaches the contract the way it reaches any chain. A Solana wallet reaches the *same* contract because a Rome chain runs inside Solana: the asset is held at an EVM address derived from your Solana key, and your signature is the only thing that can drive it.
* **The asset holds the rule.** Who may hold it is enforced by the contract on every transfer — not by this site and not by a bridge. An address the issuer has not approved simply cannot receive it.

***

## Browsing open assets

![The open-assets list at /invest, with price and inventory read live from the chain.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-18422483cf862d7243349361c7dcbd4d9c6b436b%2Finvest-list.png?alt=media)

`/invest` lists every asset with a sale open right now. For each row you see:

| Column    | Meaning                                                |
| --------- | ------------------------------------------------------ |
| **Asset** | The asset's name and symbol, read off the token itself |
| **Price** | The price of one unit, in `wUSDC`                      |
| **Sold**  | Units sold of units offered, with a progress bar       |

The summary above the table counts **Open now** (how many sales are live), **Issued here** (how many assets this chain has issued) and **Paid in** (`wUSDC`, the cash token for every sale on this deployment).

> **Nothing on this page is baked.** Price, inventory and status are read live from the chain on every visit. If the chain does not answer, the page says so — *"The chain did not answer…"* — rather than showing a stale number or claiming emptiness. *"Nothing is on sale right now"* is a claim about the chain, and it appears only when the chain actually returns an empty list.

You can read prices and inventory without connecting. To buy, you will need a wallet: the page shows a **Connect wallet** prompt, offering your EVM and Solana wallets under two headings. Selecting a row opens that asset's detail page.

***

## One asset

![An asset detail page: the facts on the left, the eligibility card and buy panel on the right.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-341ebd012e46da597a5832fe6ba39af8704be545%2Fasset-detail.png?alt=media)

An asset page (`/invest/[address]`) has two halves: the asset's facts and rules on the left, and your eligibility and the buy panel on the right.

### The facts

| Fact              | Meaning                                            |
| ----------------- | -------------------------------------------------- |
| **Price**         | Price of one unit, in `wUSDC`                      |
| **Available**     | Units still available in the open sale             |
| **Yield paid in** | The currency any yield is distributed in (`wUSDC`) |

### Who may hold it

The page states three rules the asset enforces:

| Rule                  | What it says                                                              |
| --------------------- | ------------------------------------------------------------------------- |
| **Permitted holders** | Addresses approved by the issuer. Checked by the asset on every transfer. |
| **Minimum**           | 1 unit. No lock-up.                                                       |
| **Transfers**         | Only to another permitted address, from either wallet world.              |

> **The asset checks this itself on every transfer. Nothing on this page can override it.** The rules are stated as facts about the token, not as this app's policy, because that is what they are.

***

## Eligibility — checked on the token, before any buy

![The eligibility card resolving on the asset page: connect, then checking, then permitted or not permitted.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-2280eee77f97ef3f1334451fb95811391fedf519%2Feligibility.png?alt=media)

Before offering you a buy, the asset page asks the asset's own allowlist module whether **your address** may hold it. The eligibility card shows one of these states:

| State                | Meaning                                                                          |
| -------------------- | -------------------------------------------------------------------------------- |
| **Connect to check** | No wallet is connected yet. Connect one to run the check.                        |
| **Checking…**        | Connected; the on-chain check has not resolved yet.                              |
| **Permitted**        | Your address may hold this asset.                                                |
| **Not permitted**    | The asset's rule refuses your address. A link offers to apply for the allowlist. |
| **Unavailable**      | The check could not be read right now (not a refusal).                           |

> **The check is two-sided.** A buy moves units *from the storefront to you*, so both parties to that transfer must be on the allowlist. Bloom asks the question that way — a one-sided check would call you eligible and then watch the buy revert because the storefront is not listed on that token.

Once your address is on the allowlist, the same card reads **Permitted**, and the buy is open to you:

![The eligibility card reading Permitted — the buyer is allowlisted, and the buy panel is live.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-46bb4f6d6d75bd6d0674c3fdbece1d8acd70f5b5%2Finvest-permitted.png?alt=media)

When you are refused, the reason shown is **the token's own**, quoted verbatim. Bloom never writes a sentence about why you were refused. If the asset has no allowlist module linked yet (a draft), it is *ungated* — nothing is restricting anyone, and you are permitted.

If you are refused, the card links to **Apply for the allowlist**.

***

## Applying for the allowlist

![The apply form: an address, a jurisdiction, a holder type, and two declarations — no documents.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-cd846bf05c44424259e0308af395f1cb3acd16ae%2Fapply.png?alt=media)

`/apply` is where you ask an issuer to admit your address. Its lede says the whole story: **No documents. Declarations only.**

> **The form holds nothing worth leaking, and writes nothing to any chain.** An application is an off-chain record the issuer decides on. The issuer runs their own checks in their own system; the allowlist write happens on their side, and all that ever reaches the chain is the yes-or-no outcome against your address.

You provide four things and no fifth:

| Field                             | Notes                                                                                                                                                      |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **The address that will hold it** | Prefilled from your connected wallet. For a Solana wallet this is your **derived** address — the address the asset sits at, not the key that signs for it. |
| **Jurisdiction**                  | One of a fixed list (Singapore, United Arab Emirates, Switzerland, United Kingdom).                                                                        |
| **You are**                       | Individual, Accredited individual, or Institution.                                                                                                         |
| **Two declarations**              | Both must be accepted: that you act for yourself and control the address, and that you accept the issuer decides who may hold the asset and may revoke it. |

The `?` next to the lede explains why no documents are asked for: the issuer needs only what they will act on, nothing you could upload could be stored here, and on chain the record is a single yes or no.

**To apply:**

1. Confirm the address, pick your jurisdiction and holder type, and accept both declarations.
2. Press **Submit application**. The form validates locally and the store re-checks everything on the server, since a form is one request away from being bypassed.
3. You are given a **private link** of the form `…/apply/status?ref=AP-XXXX-XXXX-XXXX`.

> **Save the link — it is the only way back to your application.** The reference is minted from 60 bits of cryptographic randomness and *is* the authorisation: nothing else stands in front of your declarations, and there is no account or password to recover them with.

On the demo, a **Test wiring** note points you at `/approve`, where you can approve your own application to complete the loop. A real issuer does this step in their own system — [see below](#approve-the-test-wiring-stand-in).

***

## Your application status, and erasing declarations

![The status page — the reference, the declared facts, the timeline, and the erase control.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-7dd1d7befa9f2e2450d783835f3d6c172bab4650%2Fapply-status.png?alt=media)

`/apply/status` reads an application back from its reference (from the `?ref=` link, or the last one this browser remembers). There is no session; the reference is the key.

The page shows where you stand — **With the issuer**, **You may hold this asset**, or **Refused** — along with a details table (reference, date applied, the address, the asset, jurisdiction, and a **Checks** row) and a short timeline of what has happened. If you have been approved, a **Buy the asset** link appears. A refusal shows the issuer's own reason, quoted.

The **Checks** row reflects the issuer's own KYC record. On this demo there is no provider wired in, so it reads *"Declarations only — no documents were asked for"* rather than implying a verification that did not happen.

### Erasing what you declared

You can erase your declarations at any time, from this page.

> **Erasing destroys the declarations and leaves the decision.** What you said about yourself is deleted and cannot be recovered. The issuer's decision stays, because it has already been acted on in public, on a chain — and unsaying that is a signature the issuer must make, not a delete you can. (An issuer's own KYC record, if any, also survives, because it is the issuer's evidence for admitting an address that is now on a public chain; erasing it here would not erase anything at their provider either.)

***

## `/approve` — the test-wiring stand-in

![The approve page — the "test wiring, not the issuer" banner, the address, the approve action, and its on-chain requirements in the fold below it.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-148d3d28590c1cf33f8ca7f3be7a980d739de48f%2Fapprove.png?alt=media)

`/approve` exists so the whole journey can be walked end to end on the demo. It carries a banner reading **"Test wiring — not the issuer"**, and that banner is the reason it is honest to have it: in production the issuer approves applications inside their own workflow, not here.

**What is real about it:** the approval is a genuine on-chain allowlist write. The page calls a deployed `TestApprover` contract that is *armed on the demo asset* (Mineral Vault I, `ARCV`, at `0x2b13ba16a429bf630f94882e477fe775d34b7c74`). That contract needs **no role** from its caller — anyone can approve any address through it — which is the entire reason it exists: it lets the demo loop close for any wallet, from either lane.

**To approve your own address:**

1. Connect the wallet you applied with (EVM or Solana). Your address is prefilled.
2. Press **Approve this address**. The write's preconditions and consequences fold below the button — open *"What this does"* to see exactly what will happen before you sign.
3. On success the page reads *"Approved. The applicant can hold the asset now."*

If you signed in with a Solana wallet, a note reads *"This will approve the address derived from your Solana key"* — the address the run signs **for** is the derived one; the key it signs **with** is your Solana key.

> **In production this is the issuer's decision, made in the issuer's system.** The demo's `TestApprover` is open by construction so the loop is walkable; a real allowlist write is gated by a role the issuer holds. Nothing about the compliance model changes — it is still the issuer's call, still one boolean on chain, and still no documents anywhere.

Once approved, return to the asset page; your eligibility now reads **Permitted**.

***

## Buying

The buy panel sits under the eligibility card on the asset page.

> **Need wUSDC first?** Every sale is priced in wUSDC, and a fresh wallet has none. [Getting funds to Rome](/apps-on-rome/bloom/funding) walks the cash-in: bridge USDC or ETH in from Sepolia, then wrap the bridged gas into wUSDC — all on the `/bridge` screen.

![The buy panel after a purchase lands — "Bought. It will show in Positions."](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-e2ffc015b988fb56be5cf364aa05ac431a0cb5a2%2Fbuy-done.png?alt=media)

1. Enter the number of **Units** you want. The panel shows the unit price and the total you will pay, in `wUSDC`. (The cash the buy actually pulls is computed from the price in base units, so the amount approved always matches the amount quoted.)
2. Press the buy control. A buy is a short sequence, not a single click: if the sale does not yet have permission to take your `wUSDC`, an **approval** step appears first (*"Allow the sale to take your cash"*), then the **Buy** itself.
3. Every step leads with its button and folds its **preconditions** and **consequences** below it — one click away before you sign — and names its phase as it runs — *"Waiting for your wallet…"*, then *"Landing on chain…"*. The sequence re-checks conditions between steps, so a sale that closes after your approval stops the buy rather than reverting it.
4. On success the panel reads *"Bought. It will show in Positions."*

From a **Solana wallet** the sequence can be longer the first time. If your Rome-side address holds less wUSDC than the buy needs, a step to move cash in from your wallet comes first, and if the wUSDC transfer grant is not yet in place an **Allow wUSDC transfers** step comes before the approval. After you press Buy, the Solana wallet runs the cash-in, the grant, the approval and the buy in one go, and the button notes "two wallet prompts" while each step signs. The step names and the prompt counts up front are shown on Trade and Credit, not here. An EVM wallet also gets the one-time **Allow wUSDC transfers** step on this panel. The steps and prompt counts are in [Paying, and the extra signature prompts](#paying-and-the-extra-signature-prompts).

If your wallet is on the wrong network, the panel says so and offers to switch to the asset's chain rather than letting a button do nothing.

> **The buy passes through the same allowlist gate as any transfer.** The storefront moves units to you only if both it and your address are permitted. There is no Bloom-side check that could substitute for the asset's own.

***

## Your positions

`/portfolio` shows what you hold. Connect the wallet that holds the asset and you get:

![The positions page — the holding, its value, and the summary.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-adabd845843de879d366f0d21b03dfb87fc0573d%2Fpositions.png?alt=media)

* a summary — **Total value** (in `wUSDC`), **Assets** (how many distinct assets you hold), and **Yield received**;
* a **Holdings** table listing each asset, your units and their value.

Everything is read live from the chain. When you hold nothing yet, the page says so and links you to the open sales. **Yield received** currently shows a dash: reading your payout history means walking the distribution events back to the asset's first block, which is work still to come — a dash is honest where a `0.00` would be a claim.

> **Positions are a view, not a market.** Bloom surfaces buying an asset from its primary sale and holding it. The asset itself permits transfers to *other permitted addresses* from either wallet world (that is one of its stated rules), but Bloom does not currently expose a secondary transfer or sell control.

***

## For a Solana holder

Everything above applies to you unchanged. This section covers the one surprise that is genuinely yours: **the asset is held at an address your Solana wallet will never list**, and why that is safe.

### The custody chain

![The Solana holder's positions — "Held for your Solana key", the derived holding address, the custody chain, and the ARCV holding bought on the Solana lane.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-b4f6ea043299e63183ceb7393e25b83948975218%2Fpositions-solana.png?alt=media)

When you connect a Solana wallet, `/portfolio` shows a **Held at** line with the EVM address your asset sits at, and a **How is this held?** toggle that opens a custody panel explaining the derivation in three steps:

1. **Your Solana key** — the key already in your wallet. It signs; it never leaves.
2. **Computes to — the address that holds the asset** — the derived EVM address. It is *arithmetic, not an account*: it follows from your key, and there is no private key for it.
3. **What can move it** — only a signature from your key can move what sits at that address.

> **Why this is safe.** The asset is EVM-state-resident: its balance is storage inside the Rome program's accounts. There is **no SPL mint of it**, no token account, and nothing Solana-native to move — so the Solana-side plumbing has no instruction that could move it out. Only your Solana signature can drive the derived address, and every movement it can express is a call into the asset contract, which runs the allowlist gate. A transfer to a non-permitted address reverts identically whether it is signed by an EVM key or a Solana key. This is exactly the property a permissioned asset wants — see [LANES.md](/apps-on-rome/bloom/lanes#custody-why-the-rwa-stays-at-the-synthetic).

Because there is no SPL token to list, **your Solana wallet will not display the RWA holding** — the Bloom app is your positions surface. A lost Solana key is recovered through the issuer's process, not by moving the asset out-of-band.

### Paying, and the extra signature prompts

* **You pay the Solana fee in SOL from your own wallet.** The derived EVM address never needs SOL. Its gas is only drawn on in the one case described below, a call too large for one transaction.
* **Every call is a Solana version 1 transaction, and every call is two prompts.** An approval, a buy or a transfer is composed for your wallet by the chain's proxy. Your wallet first signs a readable message that begins *Rome authorization*, which proves your Solana key authorizes this exact call. It then signs the transaction itself. Approve then buy is therefore four prompts.
* **A wallet that cannot sign version 1 transactions stops there.** Bloom fails closed instead of sending an older format. Use Solflare on devnet for now; Phantom does not sign version 1 transactions yet.
* **Moving cash in is one prompt, only when needed.** A buy spends wUSDC from your derived address, which holds nothing at rest. If it holds less than the buy costs, Bloom first moves the shortfall from your wallet's own USDC token account. On Trade and Credit this step is named *Transferring your cash*; on the Invest buy it runs as part of the buy after you press Buy.
* **The one-time grant is a step of its own, once per wallet.** The first time your derived address spends wUSDC, Bloom puts **Allow wUSDC transfers** ahead of the approval. It is a call like any other (two prompts), and it is not asked again once it is in place. Why it exists is in [One-time grant per wallet](/core-concepts/token-interop#one-time-grant-per-wallet).
* **A call too large for one Solana transaction is relayed, with fewer prompts.** Bloom always tries the direct, wallet-signed transaction first. If the chain refuses it because it does not fit, the same authorization signature is handed to the proxy, which submits it for you: one prompt for that call. Because the proxy submits it, the call is paid in Rome gas from your derived address, so if that balance is short Bloom first tops it up from your wallet, which is one more prompt, normally once per wallet. See [LANES.md](/apps-on-rome/bloom/lanes#calls-too-large-for-one-transaction).

A first buy from a fresh Solana wallet is therefore: move cash in (1 prompt), allow wUSDC transfers (2), approve the sale (2), buy (2).

> **Larger actions are relayed, not refused.** A buy fits one Solana transaction, and a "multi-step journey" (approve, then buy) is several whole transactions, each atomic on its own. Only a call the proxy reports as too large for one transaction, which in Credit can include the larger market actions, takes the relay route described above.

***

## Where the compliance really is

To summarise what is enforced where, so nothing here is mistaken for a Bloom policy:

| Concern                              | Enforced by                                     | Not by                     |
| ------------------------------------ | ----------------------------------------------- | -------------------------- |
| Who may hold the asset               | The asset's allowlist module, on every transfer | This website               |
| A transfer to a non-approved address | Reverted on chain by the asset                  | Any off-chain filter       |
| What is stored about you             | One boolean (`isWhitelisted`) per address       | Documents or PII, anywhere |
| Who may admit you                    | The issuer's off-chain decision                 | Bloom                      |

For the full model, read [COMPLIANCE.md](/apps-on-rome/bloom/compliance); for the two-lane mechanics, [LANES.md](/apps-on-rome/bloom/lanes).

> **This is not a mock-up.** A real buyer bought this asset from an EVM wallet, and a real Solana wallet bought it on the Solana lane — the same token, the same allowlist, the same wUSDC. Every transaction is linked on the [Evidence](/apps-on-rome/bloom/evidence-investor) page. The Permitted card, the positions, and the Solana custody chain in this guide are those buyers, after those transactions.


# Getting funds to Rome

Every sale on Bloom is priced in **wUSDC**, the cash token for this deployment, and each transaction costs a little **Rome gas**. Before the [Investing](/apps-on-rome/bloom/investing) journey — browse, apply, buy — you need both in the wallet you will buy with. This page is how you get there.

> **There is no faucet that mints wUSDC on Rome directly.** Rome's gas is *bridged* value, not printed. So the cash-in for an EVM wallet is a real bridge from a testnet you can already fund (Sepolia), and then one wrap. Four steps take you from an empty wallet to a bought asset: testnet funds, the bridge, the wrap, then the buy. The app walks you through the last three.

The whole cash-in lives on one screen, **`/bridge`**, reachable from **Bridge** in the top navigation. This is the **EVM** path — a Solana wallet needs none of it and buys straight from its own USDC and SOL, covered in [the Solana section](#the-solana-wallet-journey-nothing-to-bring-across) below.

***

## The EVM wallet journey

Four steps take you from an empty wallet to a bought asset. The first is on Sepolia (off Bloom); the middle two are the `/bridge` screen; the last is the ordinary buy.

### 1 · Get test USDC on Sepolia

Bloom bridges **USDC** (via Circle's CCTP) or **ETH** (via Wormhole) *in* from Sepolia, so you start with testnet funds there:

* **Test USDC** — from Circle's testnet USDC faucet, to the EVM address you will bridge with.
* **A little Sepolia ETH** — from any Sepolia faucet. You need it to pay the gas for the Sepolia-side transactions that start the bridge (the approve and the burn).

> **You sign the source transaction yourself, on Sepolia.** The bridge is an off-chain orchestrator, not a custodian — no key of Bloom's can move your funds. The burn is signed in your own wallet; only then does the value settle on Rome.

### 2 · Bridge it to Rome

![The bridge, under its heading — pick USDC or ETH, enter an amount, see what you will receive on Rome, and the action lights gold once the amount is valid.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-5449eb661d3fbc0383c7025c172fdcbe3e69d34d%2Fbridge-inbound.png?alt=media)

On `/bridge`, connect an **EVM** wallet (the source burn is signed on Sepolia, so a Solana wallet cannot bridge here — the screen says so). The bridge and the wrap sit **side by side**, each under its own heading. Then:

1. Pick the asset — **USDC** (via CCTP) or **ETH** (via Wormhole).
2. Enter an amount. The panel shows what you will receive on Rome, and the **Bring it to Rome** button lights **gold** the moment the amount is valid.
3. Press it. A live tracker appears and names **every wallet step honestly** — for USDC that is four: *switch to Sepolia*, *approve the bridge*, *burn on Sepolia*, *authorise the settlement*. Each index fills **done** as it lands, the current one reads *in progress*, so the count on screen is the count in your wallet.

Settlement on Rome takes a few minutes. The tracker's last row, **Settling on Rome**, stays *in progress* until the pod confirms — then the panel reads **arrived on Rome ✓** in green, and the value is at **your own address** on Rome.

> **Bridged USDC arrives as native Rome gas, not as wUSDC.** Circle's CCTP settles the inbound USDC as the chain's gas token. That is the right thing for paying for transactions — but the storefront prices assets in **wUSDC**, so there is one more move before you can buy.

### 3 · Wrap gas into wUSDC

![The wrap card, under its own heading — a direction toggle (Gas → wUSDC / wUSDC → gas), an amount, and the gold action.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-a46a955c62e940ab9f5cba87b0fbed77a53fc5ae%2Fbridge-wrap.png?alt=media)

The **wrap card** sits **beside** the bridge on `/bridge`, under its own heading, *Ready your cash to buy*. It converts the native gas you just bridged into the wUSDC the storefront takes:

1. Leave the direction on **Gas → wUSDC** (the reverse, **wUSDC → gas**, is there for when you want gas back).
2. Enter how much to wrap. Keep a little as gas for transaction fees; wrap the rest to spend.
3. Press **Wrap to wUSDC**. It is a **single Rome transaction** — no bridge, no attestation, unlike the bridge. The button reads *Waiting for your wallet…*, then *Confirming on Rome…*, and when the receipt lands it confirms **in green**: *✓ Wrapped — you now have about N wUSDC to buy an asset*, with a link to the tx. A reverted wrap says so plainly — it never reads as success.

![The wrap card after a real wrap — the green confirmation "✓ Wrapped — you now have about 2 wUSDC to buy an asset", linking the transaction.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-496c1f40006c20e85b65431b10dc16b7140e169c%2Fbridge-wrap-sent.png?alt=media)

> **This wrap is the gas-backed cash mint, and only for wUSDC.** wUSDC *is* the chain's gas value in ERC-20 form, so wrapping is a 1:1 conversion of gas you already hold. Other wrappers (wSOL, wETH) are their own token accounts and need no wrap step — this card is the wUSDC path specifically, the one the buy needs.

### 4 · Buy the asset

You now hold **wUSDC**. Go to [Investing](/apps-on-rome/bloom/investing) — `/invest`, pick an asset, clear the eligibility gate, and buy. The buy pulls wUSDC and, the first time, asks you to approve the storefront to take it. From here the journey is the ordinary one.

***

## Evidence — a real wrap, on chain

The wrap card is not a mock-up. A real wrap was signed through it in the browser and read back off the chain — the balance moved, not just a receipt returned.

* **Wallet:** [`0x5667b3700b4a021Cc475157cbf3dE2d756Fae597`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x5667b3700b4a021Cc475157cbf3dE2d756Fae597)

| What it did                         | Transaction                                                                                                                                 | On-chain result                                       |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| Wrap 2 gas → wUSDC through the card | [`0x995c96b0…37de972b`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x995c96b0e02d423b0dc9438f04a170bb8bc97a54059ab873b7e3cfde37de972b) | wUSDC balance `4.733593` → `6.733593` (**+2.000000**) |

The `bridge-wrap-sent` screenshot above is this transaction, in the state the card showed when it landed.

***

## The Solana wallet journey — nothing to bring across

For a Solana wallet there is **no bridge and no wrap**, and that is the entire point. A Solana user already holds **USDC and SOL in their own wallet**. They connect, and they buy — signing with their Solana key, paying the fee in SOL, spending the cash their key already controls. Nothing is bridged and nothing is wrapped. The buy itself moves the cash it needs into the wallet's Rome address as its first step, and a first-time wallet also sees a one-time *Allow wUSDC transfers* step; both run as part of the buy after you press Buy, and both are described in [Investing](/apps-on-rome/bloom/investing#paying-and-the-extra-signature-prompts).

* **No bridge, no wrap.** The EVM journey above exists only because value starts on Sepolia and has to be carried across. A Solana user's value is already in their wallet; there is nothing to carry.
* **Fees in SOL, from your own wallet.** Each direct call is one Solana transaction, and its fee is paid in lamports directly — the SOL a Solana user already has. A call too large for one transaction is relayed by the chain's proxy and is paid in Rome gas from the wallet's Rome address, topped up from the wallet once if it is short.
* **Spent from your own key.** Only your Solana signature moves your cash or your asset. The two-lane mechanics — one deployment, the asset controlled by your key, no second token — are in [Investing → For a Solana holder](/apps-on-rome/bloom/investing#for-a-solana-holder), and a real Solana buy of the same asset is on the [Evidence](/apps-on-rome/bloom/evidence-investor) page.

> **If a Solana user had to bridge to Rome first, the lane would be pointless.** The whole value of the Solana-native lane is that nothing changes for the user: they interact with their own wallet, with the USDC and SOL already in it, and buy. The cash-in steps on this page are the **EVM** path — the one journey that genuinely has to cross a bridge.

***

## Where the money is, at a glance

| Your wallet              | You start with                    | To buy, you…                                                                                                                                                                                  |
| ------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **EVM**, from Sepolia    | Test USDC on Sepolia              | Bridge in (step 2), wrap to wUSDC (step 3), then [buy](/apps-on-rome/bloom/investing#buying)                                                                                                  |
| **EVM**, already on Rome | Native gas, bridged in            | Wrap to wUSDC (step 3), then [buy](/apps-on-rome/bloom/investing#buying)                                                                                                                      |
| **Solana**               | USDC + SOL already in your wallet | Just [buy](/apps-on-rome/bloom/investing#buying) — no bridge, no wrap; the buy moves the cash in as its first step. Sign with a Solana wallet that signs v1 transactions (Solflare on devnet) |


# Issuing

Bloom lets you issue a **permissioned real-world asset** as an ordinary Solidity contract on a Rome chain, and administer it from a per-asset console. The asset is reachable from both EVM and Solana wallets, but **issuing is EVM-only** — a Solana wallet holds assets; it does not create them. This guide covers the issuer console, the creation wizard, the console sign-in gate, and each of the six administration tabs.

> **You do not hand Bloom any documents or personal data, and neither does your investor.** Bloom records the *outcome* of your compliance decision; the chain records one boolean per address. Your KYC/AML process — your vendors, your files, your retention — stays entirely in your own system. The full model is in [COMPLIANCE.md](/apps-on-rome/bloom/compliance); this guide points to it rather than restating it.

***

## What you can do

As an asset's administrator, everything below is yours to do from the console — and every one of these is a real on-chain transaction, linked on the [Evidence](/apps-on-rome/bloom/evidence) page:

* **Issue an asset** — the wizard deploys it as three contracts (below).
* **Review applications** — approve or refuse each one (a refusal carries a reason the applicant is owed), and admit an approved address to the allowlist.
* **Manage the allowlist** — add one address, add **a batch of addresses in a single transaction** (`batchAddToWhitelist`), or remove an address; the token enforces the list on every transfer.
* **Gate the asset** — restrict transfers to the allowlist (which makes supply final), or lift the restriction.
* **Manage supply** — read the total, and mint or burn once you hold the role.
* **Run the sale** — open a sale (units and price), close it, recall unsold inventory, and withdraw the proceeds.
* **Set and distribute yield** — set the payout currency, then distribute a payout, which walks holders one transaction at a time; excluded holders are skipped.
* **Manage roles** — grant a role to an address, revoke one, or renounce your own.

Each is detailed below, with what the chain enforces, in the tab it belongs to.

***

## The issuer console

![The issuer console at /issuer, listing the assets this deployment knows and their sale state.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-c94de7c7a94a287a4d9b90a92b2b16022213b702%2Fissuer-console.png?alt=media)

`/issuer` lists the assets you have issued on this chain and lets you start a new one.

* Connect an **EVM wallet**. A disconnected visitor, or a Solana wallet, is shown a prompt to connect the right kind of wallet rather than an empty console — a Solana wallet cannot issue, and that is a product boundary the chain enforces as a second, independent guard.
* The table lists each asset with its **name and symbol**, its lifecycle **phase**, and its **Sale** state (an *Open* badge or *Closed*).

Two columns — **Holders** and **To review** — show a dash. The holder count needs the allowlist's enumeration and the review queue is an off-chain store you sign into per asset; a dash says *not asked* where a `0` would claim a fact the app has not read.

Press **Issue an asset** to open the wizard, or select a row to open that asset's console.

***

## Creating an asset: the wizard

![The create wizard at /create: the asset fields, then six cards that light up as their transactions land.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-94683a3a49ac2418aed8cc66dbf47fad940814f7%2Fcreate-wizard.png?alt=media)

`/create` collects four fields and then deploys your asset.

| Field        | Rule                              |
| ------------ | --------------------------------- |
| **Name**     | Required.                         |
| **Symbol**   | Required.                         |
| **Supply**   | A whole number greater than zero. |
| **Decimals** | A whole number from 0 to 18.      |

Press **Start**. If a field is invalid, the wizard tells you which and why rather than sitting behind a disabled button. When the draft is valid, your wallet is asked to sign each step in turn.

### Why it deploys piecewise

> **A token is deployed as three contracts across several transactions — never one.** Arc's one-transaction `createToken` would need **77 account locks**, and Rome caps a single transaction at **62 locks**. So the wizard deploys each piece in its own transaction and gains factory status afterwards via `ArcTokenFactoryV2.registerToken`. This is a Rome constraint made visible, not an optional optimisation — and the guards preserve Arc's upstream security model exactly (canonical proxy, whitelisted implementation, caller holds the token's admin role, registration is once-only). See [COMPLIANCE.md](/apps-on-rome/bloom/compliance#what-rome-adds-and-what-registertoken-preserves).

### The six cards

The wizard shows six cards, which follow the deployment's progress. A card turns from **Waiting** to **Signing…** to **Done** (or **Failed**) as its transaction lands.

| # | Card                        | What it does                                                                                             |
| - | --------------------------- | -------------------------------------------------------------------------------------------------------- |
| 1 | **Implementation**          | The shared asset implementation for this chain — resolved, nothing to sign. (Done before any step runs.) |
| 2 | **The asset**               | Deploys your asset and sets its name, symbol, supply and decimals in one step.                           |
| 3 | **Who may hold it**         | Deploys this asset's own permission module, with you as its administrator.                               |
| 4 | **Enforce it on transfers** | Points the asset at that module, so every transfer is checked by the asset itself.                       |
| 5 | **Yield rules**             | Deploys and links the module that decides who is paid.                                                   |
| 6 | **Registration**            | Registers the asset — this is what opens the sale and lets you upgrade it later.                         |

The six cards fold eight underlying deployment steps, so a single **Start** results in **eight signatures**. A note under the button reads *"Your wallet will ask you to sign each step. Stop at any point and pick up where you left off — nothing is lost."* Registration being the final step is why a stop is safe: the asset is a real deployment at each stage, registered at the end.

When the run completes, the wizard reads *"Issued. Your asset is live at …"* with a link to the asset's console at `/token/[address]`.

> **`initialize` grants you every role.** The wizard driver — your EVM address — receives the token's admin, minter/burner, yield-manager/distributor and upgrader roles, plus the allowlist module's admin role. Your key custody is therefore part of the asset's compliance posture: a lost investor wallet is recoverable only through your admin action, and the console's **Rights** tab makes every power's holder visible. (`UPGRADER_ROLE` is *not* granted to the factory automatically — grant it explicitly if you want factory-mediated upgrades.)

***

## The per-asset console, and the sign-in gate

![The console sign-in gate: sign a message with your wallet — there is no password.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-a9377e3cad8460af1d9bb4eb3cf6f2a6a8f28b22%2Fconsole-signin.png?alt=media)

Opening `/token/[address]` does **not** show the console straight away. It shows a **sign-in gate**:

> *"Sign in to manage this asset. Sign a message with your wallet — there is no password. Your authority comes from the asset itself, and it is checked again on every action, so revoking it takes effect at once."*

Press **Sign in with your wallet**. Here is exactly what happens:

1. Your wallet is asked to `personal_sign` a short, human-readable statement (headed *"Bloom — prove you manage this asset"*) that names the one action it authorises — reading this asset's application queue — along with the asset address, the chain id, and a timestamp.
2. The app sends that signed statement to `GET /api/applications`.
3. The **server** verifies it: it recovers your address from the signature, confirms the statement is for this chain and still fresh (it authorises for a two-minute window, not a session), reads the **token's own allowlist module** from the chain, and checks that your address holds the role that the allowlist write actually requires — `MANAGER_ROLE` on that module.
4. If you hold it, the queue is returned and the console renders. If you do not, the server refuses with `403` and a reason.

![The gate showing a quoted refusal: a wallet that does not manage this asset is turned away.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-99040b8d3c752f0f84615b913d6b75fb0d36f81b%2Fconsole-refusal.png?alt=media)

> **The asset decides who may administer it — not this app.** The role is checked on the allowlist **module**, not on the token, and holding a same-named role on the token does *not* carry over — they are different authorities. There is no account and no session: every request that follows carries its own fresh signature, which is what *"checked again on every action"* means. Revoke a wallet's role on chain and it stops being able to sign in immediately.

A refusal shows the reason **quoted** — Bloom never writes a sentence about why a wallet was refused. If you decline the signature in your wallet, that is your *"no"*, not the asset's, and it is not reported as a refusal.

> **The gate is whose-console-it-is, not what-is-secret.** Supply, sale state, the allowlist roster and the roles are public chain reads any explorer will answer. What the signature buys is the **application queue** — other people's declarations — and the right to be shown the console at all.

### The console header

Once signed in, the header names the asset and shows its **phase** — the most consequential fact it has:

| Phase     | Meaning                                                                                                               |
| --------- | --------------------------------------------------------------------------------------------------------------------- |
| **Draft** | No permission module is linked yet, so nothing restricts who may hold this.                                           |
| **Open**  | Transfers are open and supply can still change. Gating the asset makes its supply final, and that cannot be undone.   |
| **Gated** | Only allowlisted addresses may hold this, **and its supply is final** — no more can be minted or burned, permanently. |

Above the tabs, a summary shows four figures — **Holders · To review · Sold · Yield paid**. Two are read from the chain and the store (*To review* once you have signed in; *Sold* from the storefront); *Holders* waits on the yield preview's holder count, and *Yield paid* waits on the payout-history cursor still to come. Each waiting cell shows a dash and explains what it needs, rather than a `0` that would claim a fact nobody asked the chain for.

***

## The six tabs

The console has six tabs, in this order: **Applications · Who may hold it · Supply · Sale · Yield · Rights.**

Every write on these tabs uses one shared control that renders the write's **named preconditions** and its **consequences** *before* the button, and names its phase as it runs (*"Waiting for your wallet…"*, *"Landing on chain…"*). Nothing should revert to teach you what could have been checked first.

### Applications — the review queue

![The Applications tab — the review queue, with a pending application to approve or refuse, and the compliance seam.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-8249de8b76f757101cc945ca4a2687103428839c%2Ftab-applications.png?alt=media)

This tab lists who has applied to hold the asset. Each row shows the applicant's address, jurisdiction, holder type, a **Checks** column (*Passed* if your provider recorded a result, else *Declared*), and a **Status** (*To review*, *Approved*, *Refused*). Expand a row to see its facts and two controls: **Approve — may hold it** and **Refuse**. A refusal requires a reason, which becomes the applicant's answer.

> **Approving is two separate acts, and the tab keeps them honest.** Pressing **Approve** *records* your decision in the application store. Putting the address on the asset's allowlist on chain is a **second signature** — `batchAddToWhitelist` — and until it lands the row says so. When the recorded decision and the chain disagree, the tab names the gap and offers the write that closes it: **Admit** to add an approved address to the allowlist, or (for a refused address still listed) the control to remove it. The address cannot hold the asset until the allowlist write lands.

A standing note on this tab — the **compliance seam** — states where your own process plugs in:

> *"No documents are asked for or held here — the checks are your own. Connect your provider behind the `Applications` interface, or import decisions you have already made. Bloom records the outcome and the chain records one yes or no against the address."*

That is the interface a self-hosting issuer replaces: swap the application store's implementation for your own durable one and no caller changes. Bloom performs no checks and holds no document.

### Who may hold it — the transfers gate

![The Who-may-hold-it tab — the allowlist count and the restrict-transfers control, showing its one-way-door consequences.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-d32c410e3b5798d86dc5b914bce2f927eb4b1620%2Ftab-holders.png?alt=media)

This tab reports how many addresses are allowlisted on the asset, and lets you flip whether the allowlist is **enforced**:

* When transfers are **unrestricted**, it offers to **restrict transfers to the allowlist**. The write's consequences state that doing so makes supply final and is a one-way door for non-admins.
* When transfers are **restricted**, it offers to **lift the restriction**. Its consequences state plainly that this is not an undo.

On an *ungated* (draft) asset with no permission module linked, the tab says so — *"No permission module is linked, so nothing restricts who may hold this"* — and the toggle does not render.

> **The token checks both sides of every transfer.** While the asset is gated, a transfer where either party is off the allowlist reverts on chain with a typed error. The same gate runs for an EVM-wallet transfer, a Solana-signed transfer, a storefront sale, and the initial liquidity move — there is no path around it short of your own admin powers.

### Supply

![The Supply tab — total units in existence.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-29eb2d741e5193eef3ea94a214a199f2eb404fe7%2Ftab-supply.png?alt=media)

This tab reports the total supply in existence, in whole units.

### Sale — open, close, and take proceeds

![The Sale tab — an open sale, with the close-sale, recall-unsold and withdraw-proceeds controls.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-573a99f9028a984b038162dc885a16b715673293%2Ftab-sale.png?alt=media)

This tab runs the primary sale through the storefront contract.

* **When no sale is open**, it offers to open one: enter **Units to sell** and **Price per unit** (in `wUSDC`), then press **Open the sale**. The write's preconditions are shown first — the token must be registered, the storefront must hold the sale inventory, and the storefront must itself be allowlisted on the token it sells — so a sale that is not yet openable says exactly what it needs.
* **When a sale is open**, it shows how many units are left of the offer and offers **Close this sale**.
* **Recall unsold units** moves unsold inventory from the storefront back to a recipient (defaulting to you). This is **not** a close — the sale stays enabled, and the write's consequence says so, so a recall cannot be misread as an unwind.
* **Withdraw proceeds** sends the storefront's takings to an address (defaulting to you). Proceeds accrue independently of the sale's state, so they can be withdrawn whether the sale is open or closed.

### Yield — pay holders, one per transaction

![The Yield tab — the distribution amount field, with the reminder to set a payout currency first.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-cc8150ea6fee72d99bd5a2254073779cbd896c11%2Ftab-yield.png?alt=media)

This tab distributes a yield payout to holders.

> **A distribution is a walk, not a click.** On Rome, `distributeYieldWithLimit` pays **one holder per transaction** (the account cap), so a payout is a sequence of windows, each its own signature. You enter the amount once, in whole units of the payout currency, and it is locked once the walk begins. The **first** window pulls the whole payout from you and pays the first holder; each later window only moves money already inside the token contract. Bloom re-reads the cursor from the chain after every window — the holder set reorders as balances change, and sending a stale index would restart at zero and pull your money a second time.

The tab shows progress — *"Paying holder N of M"* — and finishes when the cursor wraps. If the asset has no payout currency set, the tab says so; setting the payout currency is the one field of a distribution only you can choose.

> **Restricted holders' shares stay in the contract.** An address excluded by the yield-blacklist module is skipped, and its pro-rata share remains in the token contract rather than being paid out.

Past payouts are not shown yet — reading them walks the distribution events back to the asset's first block, which needs a history cursor still to come. The tab says this inside itself rather than showing a `0`.

### Rights — the roles

![The Rights tab — every role and whether it is held, and the grant / revoke / renounce control.](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-fb54f6c73c2d194dd5b6321bc3cb0af825f6f637%2Ftab-rights.png?alt=media)

This tab lists every role the token declares and whether it is held, each read with a single `hasRole` call against the token. A role that is **not** held shows as such rather than being omitted — on a rights tab, a missing row and a *"no"* are different facts.

Below the list, a control changes the roles:

| Action                     | Acts on                                                            | Authority            |
| -------------------------- | ------------------------------------------------------------------ | -------------------- |
| **Grant to an account**    | An address you enter                                               | `DEFAULT_ADMIN_ROLE` |
| **Revoke from an account** | An address you enter                                               | `DEFAULT_ADMIN_ROLE` |
| **Renounce your own**      | Yourself (OpenZeppelin requires the confirmation to be the caller) | You                  |

The role choices are the token's own, so the control can never offer a role the token does not have. As with every write, the authority row and the consequence render before the button, so the door and its one-way-ness are stated before you sign.

***

## What never touches Bloom or the chain

To be unambiguous about where compliance lives:

| Thing                                | Where it lives                                                                         |
| ------------------------------------ | -------------------------------------------------------------------------------------- |
| KYC/AML documents and PII            | Your own system, with your own vendors and retention — **never** this app or the chain |
| Your decision to admit an address    | An off-chain record; you own the store                                                 |
| The on-chain effect of that decision | One boolean — `isWhitelisted(address)` — written by `batchAddToWhitelist`              |
| Enforcement of the rule              | The asset contract, on every transfer                                                  |
| Who may administer the asset         | The role held on the asset's allowlist module, checked on every request                |

For the compliance model in full, read [COMPLIANCE.md](/apps-on-rome/bloom/compliance); for the two-lane (EVM and Solana) mechanics your holders use, read [LANES.md](/apps-on-rome/bloom/lanes).


# Building on Bloom

This guide answers one question: **how does a builder do this themselves?** Take a Solidity real-world-asset contract, stand it up on a Rome chain as a permissioned token, open it to Solana wallets as well as EVM wallets with no bridge and no second token, and plug in your own KYC/compliance decision.

The audience is a developer evaluating or adopting the approach. Bloom is the worked example throughout — its live asset **ARCV ("Mineral Vault I")** on Hadrian (`200010`) is a real instance of every step below, and its deploy receipt (`deployments/200010.tokens/ARCV.json`) records the exact three contracts and eight transactions this method produces.

Nothing here modifies the asset contracts. Bloom runs Plume's Arc framework **unmodified** (the vendored tree in `contracts/`, byte-identical to `plumenetwork/contracts` at the pin in `NOTICE`); the only Rome-side contract is `ArcTokenFactoryV2`, which adds one function. The work is in *how* you deploy and *how* you drive the contracts — not in changing them.

> **The method, at a glance.** A permissioned token is **three** deployed contracts. It is deployed **piecewise** — one contract per transaction — because a one-transaction factory call exceeds Rome's per-transaction account cap. Its proxies must come from **provenance-locked artifacts** or on-chain registration fails closed. Compliance is one **boolean per address**, written by whoever your KYC process authorises. And the same deployment is reachable from a Solana wallet through a **synthetic sender** derived on-chain — no bridge, no wrapped token, no second allowlist.

## Prerequisites

> **Before you start.** The issuer tooling this guide uses is not published yet. That covers the scripts (`deploy-infra.ts`, `create-token.ts` and the rest), the `contracts/` and `rome-contracts/` trees, and the deploy receipts it names. Issuers get this tooling by onboarding with Rome. Until then, read the guide as a description of the method and of what each script does.

The reference tooling lives in a `scripts/` directory (provided at onboarding; install its dependencies with `npm install`). Every command resolves chain facts from the registry rather than hardcoding them, so retargeting another Rome chain is an environment change, not a code change:

```bash
export PRIVATE_KEY=…       # funded deployer/issuer key on the target chain
export CHAIN_ID=200010     # picks the chain
export REGISTRY_ROOT=…     # a checkout of the Rome chain registry
# Solana-lane flows also need:
export SOLANA_KEYPAIR=…    # path to a funded Solana keypair JSON (the fee payer)
```

Gas on Rome chains is USDC-denominated; the full deploy sequence costs on the order of 10–15 native units on devnet. Keys come from the environment only — never the repo. Build the contracts once before deploying, because artifacts **must** come from `rome-contracts/out` (see step 3):

```bash
cd contracts       && forge build --via-ir     # the vendored Arc suite
cd rome-contracts  && forge build --via-ir     # ArcTokenFactoryV2 + the canonical proxy
```

***

## Step 1 — Know what a permissioned token *is* here

A permissioned Bloom token is not one contract. It is **three deployed contracts**, plus four shared contracts that ops deploys once per chain and every issuer reuses.

**The three per-token contracts** — one asset owns exactly these:

| Contract                     | Source                                                      | What it is                                                                                                                |
| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | The token itself — a UUPS proxy pointing at the shared `ArcToken` implementation. Balances, holders, and roles live here. |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | The allowlist module. One boolean per address; enforced on every transfer while the asset is gated.                       |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | The yield module. Excludes addresses from yield distributions without touching their holding.                             |

**The four shared, one-per-chain contracts** (deployed by `deploy-infra.ts`):

| Contract                    | Role                                                       |
| --------------------------- | ---------------------------------------------------------- |
| `RestrictionsRouter`        | Module-type registry — chain wiring, set once by ops.      |
| `ArcTokenFactoryV2`         | Registration + the canonical proxy codehash (steps 2–3).   |
| `ArcToken` (implementation) | The shared, reusable asset logic every token proxies to.   |
| `ArcTokenPurchase`          | One storefront, shared by every token and issuer (step 4). |
| wUSDC                       | The cash leg — yield currency and sale-purchase currency.  |

Transfer restriction and yield restriction are **deliberately separate modules**: a holder can be barred from income while keeping the asset, which is how sanctions and court orders actually work.

This "three contracts" fact is load-bearing for verification. `verify-deployments.ts` and `register-asset.ts` both expect a token to resolve to three deployed contracts — verifying a token by hand once missed seven of them across a chain (every yield module and three whitelist modules).

The lifecycle has three phases and a one-way door:

```
draft     proxy live, no module linked — nothing gates a transfer
  ↓
ungated   module linked, transfersAllowed = true   (WhitelistRestrictions.initialize sets this)
  ↓  mint supply, build the allowlist               [reversible]
gated     setTransfersAllowed(false)                [ONE-WAY DOOR — supply becomes final]
```

Gating is one-way for supply because mint and burn pass `address(0)` as the counterparty, and `address(0)` can never be allowlisted — so once gated, no new supply can ever be minted. That is the guarantee a permissioned asset makes to its holders.

***

## Step 2 — Deploy it piecewise, never as one transaction

Arc ships a one-shot `createToken` that deploys the proxy and both modules and wires them in a single call. **On Rome you cannot use it.** That transaction needs **77 Solana account locks**, and Rome's per-transaction cap is **62 locks**. It can never fit — not with tuning, not with an optimizer pass.

So the deploy is **piecewise**: each contract in its own transaction, and the token gains factory-equivalent status through `ArcTokenFactoryV2.registerToken` instead of the monolithic `createToken`. This is the create wizard, and it is what the issuer console does step by step:

![the create wizard](https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-94683a3a49ac2418aed8cc66dbf47fad940814f7%2Fcreate-wizard.png?alt=media)

The reference implementation is `arc/plan/create.ts` (`createSequence()`), which the app renders as six cards (`bloom/lib/createCards.ts`) over **eight transactions**, in the exact order the contracts force:

| # | Transaction                  | Contract call                                                                   |
| - | ---------------------------- | ------------------------------------------------------------------------------- |
| 1 | Deploy the asset             | `new ArcTokenProxy(impl, initData)` where `initData = ArcToken.initialize(...)` |
| 2 | Deploy the allowlist module  | `new WhitelistRestrictions()`                                                   |
| 3 | Set you as its administrator | `whitelist.initialize(issuer)`                                                  |
| 4 | Enforce it on transfers      | `token.setRestrictionModule(TRANSFER, whitelist)`                               |
| 5 | Deploy the yield module      | `new YieldBlacklistRestrictions()`                                              |
| 6 | Set you as its administrator | `yieldBlacklist.initialize(issuer)`                                             |
| 7 | Enforce it on yield          | `token.setRestrictionModule(YIELD, yieldBlacklist)`                             |
| 8 | Register with the factory    | `factoryV2.registerToken(token, impl)`                                          |

The script form is one command:

```bash
# one-time per chain (ops, not per token) — deploys the four shared contracts
npx tsx deploy-infra.ts

# per token — the wizard, replayed as a script
NAME="Mineral Vault I" SYMBOL=ARCV SUPPLY=1000000 DECIMALS=6 npx tsx create-token.ts
```

The issuer (`PRIVATE_KEY`) signs every transaction and ends up holding the token's roles (`ArcToken.initialize` grants to `msg.sender` — see step 7). Registration (tx 8) is what unlocks the storefront's `enableToken` and any factory-mediated upgrade.

> **Do not "optimize" this back into a monolith.** Batching these calls into one transaction to save round-trips reintroduces exactly the 62-lock overflow the piecewise path exists to avoid. The same cap is why yield distribution walks one holder per transaction (step 8). If a call fails with `Too many accounts: N > 62`, one transaction is touching too many accounts — split it, don't tune it.

***

## Step 3 — Deploy proxies only from the provenance-locked artifacts

`registerToken` is the security boundary that lets piecewise deploys be safe. It will only accept a token whose **runtime codehash matches the canonical `ArcTokenProxy`** baked into the factory at deploy time:

```solidity
// rome-contracts/src/ArcTokenFactoryV2.sol
bytes32 private immutable CANONICAL_PROXY_CODEHASH =
    keccak256(type(ArcTokenProxy).runtimeCode);

function registerToken(address token, address implementation) external {
    if (token.codehash != CANONICAL_PROXY_CODEHASH) revert ProxyCodehashUnknown();
    // …implementation must be factory-whitelisted, and msg.sender must hold the
    //    token's ADMIN_ROLE, and the token must not already be registered.
}
```

Because that codehash is an **immutable**, a proxy built from any other artifact fails closed with `ProxyCodehashUnknown`. Two rules follow, and both are enforced by CI and the hard rules in `CLAUDE.md`:

1. **Token proxies MUST be deployed from `rome-contracts/out`** — never from `contracts/out`, never from a different build. That is the compilation the factory's canonical codehash pins.
2. **`bytecode_hash = "none"` and `cbor_metadata = false`** in `rome-contracts/foundry.toml` are load-bearing. With metadata enabled, the `ArcTokenProxy` runtime *embedded* in the factory (`type().runtimeCode`) carries a different CBOR/IPFS tail than the standalone `out/` artifact the wizard deploys from — so on-chain `registerToken` reverts while **every in-source Foundry test passes** (Foundry embeds both copies, so it can't see the mismatch). `rome-contracts/test/ArtifactProvenance.t.sol` pins this property; the funded run is what originally caught it.

> **A green in-source suite does not prove provenance.** Before shipping an image, and after ANY compiler-setting change, prove the property against the *deployed* factory (read-only, no keys):
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> It hashes your local `ArcTokenProxy` artifact and asserts that hash appears verbatim inside the deployed factory's bytecode.

If compiler settings must change, **rotate the factory** — deploy a fresh one whose immutable pins the new codehash, and re-wire — rather than hand-patching the check:

```bash
cd scripts && CHAIN_ID=<id> npx tsx rotate-factory.ts
```

Rotation retains the old address in the receipt; tokens registered against the old factory must re-register against the new one.

***

## Step 4 — Open a sale through the shared storefront

Selling is a separate act from creating, and it runs on the one shared `ArcTokenPurchase` storefront. Three things must be true before a sale opens, and `ArcTokenPurchase.enableToken` checks each on-chain:

```solidity
// contracts/src/ArcTokenPurchase.sol
function enableToken(address _tokenContract, uint256 _numberOfTokens, uint256 _tokenPrice)
    external onlyTokenAdmin(_tokenContract)                       // (a) you hold the token's ADMIN_ROLE
{
    // (b) the token must be factory-known — this is what registerToken (step 2) bought:
    if (ArcTokenFactory(ps.tokenFactory).getTokenImplementation(_tokenContract) == address(0))
        revert TokenNotCreatedByFactory();
    // (c) the storefront must already hold the sale inventory:
    if (ArcToken(_tokenContract).balanceOf(address(this)) < _numberOfTokens)
        revert ContractMissingRequiredTokens();
    // …price and count must be positive.
}
```

There is a fourth, implicit requirement that a gated asset forces: because **every transfer runs the allowlist gate**, moving inventory to the storefront only succeeds if the storefront address is itself whitelisted on your token. So the full open-a-sale sequence is:

1. **Whitelist the storefront** on your token's whitelist module (`batchAddToWhitelist([storefront])` — see step 5). Without this, the inventory transfer in (2) reverts `TransferRestricted()` once gated.
2. **Transfer inventory** to the storefront address (from the infra receipt's `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** — buyers now pay wUSDC and receive the RWA.

The withdrawal split is worth knowing on a *shared* storefront, and it is not symmetric (`arc/roles.ts` is the authority):

| Action                                                     | Authority                                   | Held by                         |
| ---------------------------------------------------------- | ------------------------------------------- | ------------------------------- |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | the token's `ADMIN_ROLE` (`onlyTokenAdmin`) | you, the issuer                 |
| `withdrawPurchaseTokens` (the wUSDC proceeds)              | the storefront's `DEFAULT_ADMIN_ROLE`       | whoever deployed the storefront |

On a self-hosted chain you are both. On a shared storefront the proceeds pool is every issuer's takings, so its withdrawal is platform-level — design your settlement around that rather than assuming unilateral control.

***

## Step 5 — Plug in your own KYC / compliance

This is the section a customer reads before saying yes, so it is code, not prose. **Compliance is enforced by the token contract itself** — not by an off-chain screen, a sequencer filter, or a venue policy. The rules travel with the token to any execution path, on both wallet lanes. What the chain records is the **outcome** of your KYC decision, as a single boolean:

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol — the on-chain state, in full
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← one boolean per address. No PII, ever.
    bool transfersAllowed;                    // false = gated; only whitelisted addresses transfer
    EnumerableSet.AddressSet whitelistedAddresses;
}
```

### The interface your process writes to

Your KYC vendor's approve/deny decision becomes exactly one of these calls:

```solidity
interface IWhitelistRestrictions {
    function addToWhitelist(address account) external;                  // onlyRole(MANAGER_ROLE)
    function batchAddToWhitelist(address[] calldata accounts) external; // onlyRole(MANAGER_ROLE)
    function removeFromWhitelist(address account) external;             // onlyRole(MANAGER_ROLE)
    function isWhitelisted(address account) external view returns (bool);
    function setTransfersAllowed(bool allowed) external;                // onlyRole(ADMIN_ROLE)
}
```

### Who may write

> **The authority an approval needs is `MANAGER_ROLE` on the&#x20;*****module*****&#x20;— not `ADMIN_ROLE` on the token.** `addToWhitelist` / `batchAddToWhitelist` / `removeFromWhitelist` are all `onlyRole(MANAGER_ROLE)`, and that role lives on the `WhitelistRestrictions` instance. Authenticating `ADMIN_ROLE` on the token and then writing to the module is a real defect this codebase has already paid for. Do not offer `WHITELIST_ADMIN_ROLE` as the fix for a refusal — it is granted at initialize and **checked nowhere in the suite** (`arc/roles.ts` asserts it is the one role that gates nothing).

`WhitelistRestrictions.initialize(issuer)` (tx 3 of the wizard) grants the issuer `DEFAULT_ADMIN_ROLE`, `ADMIN_ROLE`, `MANAGER_ROLE`, `WHITELIST_ADMIN_ROLE`, and `UPGRADER_ROLE` on that module. So the issuer holds `MANAGER_ROLE` out of the box and can also delegate it: because the issuer holds `DEFAULT_ADMIN_ROLE`, a `grantRole(MANAGER_ROLE, serviceAccount)` hands the write to a backend signer.

### A decision becoming an on-chain write

Point your vendor's webhook at a signer that holds `MANAGER_ROLE`, and on an approve, batch the addresses:

```typescript
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

// `signer` must hold MANAGER_ROLE on THIS token's whitelist module.
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// vendor said "approved" for these addresses:
await wallet.writeContract({
  address: whitelistModule,                 // resolved FROM the token via getRestrictionModule
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

Resolve `whitelistModule` from the token itself (`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`), never from a cached projection — aiming a write at the wrong module is how "approve on any asset" once wrote to one token's allowlist.

### The wallet-signed variant (self-serve demo)

For a permissionless test deployment where visitors admit themselves, grant `MANAGER_ROLE` to a `TestApprover` (`rome-contracts/src/TestApprover.sol`) whose `module` is **immutable**. Then `approve(address)` is callable by anyone, from their own wallet, paying their own gas — it calls `addToWhitelist` only if the address is not already listed:

```solidity
function approve(address account) external {
    IWhitelistRestrictions target = IWhitelistRestrictions(module);   // immutable — one module, forever
    if (!target.isWhitelisted(account)) target.addToWhitelist(account);
    emit Approved(account, msg.sender);
}
```

This is the "sign-in gate" — it shows a visitor the real mechanism (an approval becoming an on-chain allowlist entry the token then enforces) without custodying anything. **Never grant `MANAGER_ROLE` to a `TestApprover` on a production token** — it makes that token's allowlist permissionless.

### When the gate bites, and swapping providers

The gate only restricts once the asset is **gated** (`setTransfersAllowed(false)`, `ADMIN_ROLE` on the module). While ungated, transfers are open — build your allowlist first, then gate. Once gated, a transfer where either side is off the list reverts with the typed error `TransferRestricted()` (`0xe827105e`).

> **Swapping KYC providers later requires no on-chain change.** The allowlist module, its interface, and the boolean it stores are provider-agnostic. To switch vendors, point a different decision source at the same `MANAGER_ROLE` signer — no redeploy, no migration, no state change. The chain never knew which vendor made the call, only the outcome.

***

## Step 6 — Open the same asset to Solana wallets

The same three contracts are reachable from a Solana wallet **with no bridge and no second token**. A Solana-native user has no EVM key anywhere; instead their EVM identity is *synthetic*, derived on-chain from the transaction's actual Solana signer:

```
synthetic EVM address = keccak256(solana_pubkey)[12..32]
```

The Rome EVM program derives this from the signer itself (`do_tx_unsigned::derive_sender`), so it cannot be spoofed and no secp256k1 private key for it exists — **the Solana signature is the only thing that can ever drive this address**. The reference derivation is `arc/materialise/solana/identity.ts` (`syntheticAddress`), byte-compatible with the on-chain rule:

```typescript
export function syntheticAddress(pubkey: SolanaPubkeyInput): `0x${string}` {
  return `0x${keccak256(pubkeyBytes(pubkey)).slice(-40)}`;
}
```

**What a builder does to support the lane** — note that *none of it is a contract change*:

1. **Whitelist the Solana user by their synthetic address.** To the compliance layer a synthetic address is an ordinary address: paste the user's Solana pubkey, derive the synthetic, and `batchAddToWhitelist([synthetic])`. One allowlist spans both wallet worlds.
2. **Provision the signing PDA once, before their first value-moving call.** The external-authority PDA that signs for a synthetic must exist first, and creating it is one `create_pda` call. Either side can send it: the user's own Solana wallet through the lane (the SDK's `buildCreatePdaCall`, after `isSyntheticProvisioned` returns false), or the issuer from an EVM key. Bloom's issuer tooling sends it today as part of onboarding a holder; the app does not yet do it on the user's first action, so provision before the holder's first buy. How, with the SDK calls, is in [Dual-lane app](/developer-guides/dual-lane-app#one-time-activate-provision-the-synthetic).
3. **Submit through the lane library, not by hand.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`) asks the chain's proxy to compose the transaction, and it handles what a hand-built one gets wrong. Every contract call on the lane is a **Solana v1 transaction composed by the proxy** (`rome_composeUnsignedAtomicV1`) with no address lookup table, and the lane library fails closed rather than fall back to an older format. The cash-in move in step 5 is a separate Solana transaction the app builds itself, not a composed contract call.
   * **Account discovery and the compute budget are the proxy's job.** It emulates the call, decides each account's writability (the call's `value` is part of what it emulates, so pass it), and sets the compute and heap budget on the transaction it returns. Nothing in your code builds compute-budget instructions or adds the fee-pool account.
   * **Two signatures per call, both from the Solana key.** The wallet first signs a readable authorization message (*Rome authorization* plus the hash of the unsigned EIP-1559 payload; Wallet Standard `solana:signMessage`), which the proxy verifies before it will compose the call. It then signs the v1 transaction the proxy returns (`solana:signTransaction` on the raw wire). The lane library takes these as `signAuthHash` and `signV1Wire`, never one collapsed method. Check that your wallet signs v1 transactions before you build on it: Solflare does on devnet today and Phantom does not yet.
   * **The app sends the signed transaction to a Solana RPC itself.** The proxy composes; it does not broadcast an atomic call.
   * **Validate before asking for a signature.** The lane library parses the composed message and refuses a malformed one before the transaction prompt.
4. **Put the one-time wrapper grant ahead of the first wUSDC spend.** wUSDC moves a user's tokens as that user's SPL delegate, so a synthetic address that has never been granted gets SPL error `0x4` on its first wUSDC transfer. Read `isEnabled(user)` on the wrapper and, when it is false, prepend `approve_spl(wrapper, 2^64-1, mint)` on the Helper precompile as the first leg of the run. It is an ordinary call on either lane, once per wallet. See [One-time grant per wallet](/core-concepts/token-interop#one-time-grant-per-wallet).
5. **Move cash in before a call that spends it.** The synthetic holds nothing at rest. When it holds less wUSDC than the call spends, move the shortfall from the wallet's own USDC token account first (an idempotent create of the synthetic's token account, then the Rome program's activate-token-account instruction, which moves the tokens without running the EVM). It is one Solana transaction signed with one prompt, and it is skipped when the synthetic already holds enough.
6. **Relay only what the atomic composer refuses.** A call that does not fit one Solana transaction (about 1.4M compute units, 64 accounts, 4 KB) is refused by the composer with *external atomic plan requires iterative composition*. On exactly that refusal, and never before, call `rome_relayUnsignedV1` with the same authorization signature: the proxy composes and submits it, pays the Solana fees, and answers with an EVM transaction hash (not a Solana signature). The synthetic pays the chain's native gas in wei, so when its balance is short of `gasLimit x maxFeePerGas`, top it up first with `rome_composeExternalDepositV1` (a wallet-signed v1 transaction; the amount must be a multiple of 10^12 wei), once per wallet in practice. Any other error, such as a revert, a network failure or a declined signature, is rethrown, not relayed. Details are in [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls).

The user is the **fee payer** and pays lamports (SOL) from their Solana wallet for a direct call; the synthetic they control holds the assets and never holds SOL. After each lane send, poll the EVM nonce before the next one — the indexer lags Solana confirmation slightly, and the next call would otherwise be built on a nonce the chain has already spent.

> **Atomic first, relay on refusal.** The default for every call is one EVM transaction executed inside one Solana transaction, signed by the user's wallet. The relay is the exception for a call that cannot fit, not a second route to pick by preference: a direct call is two prompts, a relayed call is one, and the relayed call is paid in Rome gas instead of lamports. A multi-step journey (approve, then buy) is several whole calls, each its own transaction, not one execution split across transactions.

**Why the RWA can't leak out of the perimeter.** The asset is EVM-state-resident — its balances are storage inside the Rome EVM program's accounts. There is **no SPL mint** of it and nothing Solana-native to move; the lane's asset-moving primitives operate on SPL token accounts, which an EVM storage balance does not have. Every movement the Solana signature can express is a call into the token contract, which runs the allowlist gate — the funded suite asserts a transfer to a non-whitelisted address reverts identically whether signed by an EVM key or a Solana key. The cash leg (wUSDC) is the deliberate exception: it is SPL-backed and *does* sweep out to the user's own token account.

***

## Step 7 — Know who holds authority after deploy

`ArcToken.initialize` runs from the proxy constructor (tx 1) and grants roles to `msg.sender` — the wizard driver, i.e. the issuer. It grants **five** roles explicitly:

```solidity
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
_grantRole(MANAGER_ROLE, msg.sender);
_grantRole(YIELD_MANAGER_ROLE, msg.sender);
_grantRole(YIELD_DISTRIBUTOR_ROLE, msg.sender);
```

Two consequences a builder must not miss:

* **`MINTER_ROLE`, `BURNER_ROLE`, and `UPGRADER_ROLE` are NOT granted at initialize.** The initial supply is minted inside `initialize` (an internal `_mint`, no role needed), but a *later* `mint` or `burn` requires the issuer to first grant themselves the role. They can, because they hold `DEFAULT_ADMIN_ROLE` (the admin of every role — nothing calls `_setRoleAdmin`, so `DEFAULT_ADMIN_ROLE` administers all of them). But it is a deliberate extra step, not an ambient power.
* **`UPGRADER_ROLE` is never granted to the factory.** Factory-mediated upgrades (`ArcTokenFactoryV2.upgradeToken`) require the **factory itself** to hold `UPGRADER_ROLE` on the token — granting it to yourself does nothing. That grant is a one-way door the factory cannot reverse, so it is opt-in: if you want factory-mediated upgrades, `grantRole(UPGRADER_ROLE, factory)` explicitly; otherwise the issuer upgrades the token directly.

The full 21-authority model (11 role names across six contracts — the same name is a different authority on each contract) is codified in `arc/roles.ts`. Ask `rolesRequiredFor(contract, functionName)` rather than reading a role's own contract, because four storefront functions and both factory functions are gated by an authority that lives on a *different* contract.

***

## Step 8 — Distribute yield

The token pays yield in wUSDC (fixed at `initialize`, changeable via `setYieldToken` under `YIELD_MANAGER_ROLE`). Distribution walks the holder set:

```solidity
function distributeYieldWithLimit(uint256 totalAmount, uint256 startIndex, uint256 maxHolders)
    external onlyRole(YIELD_DISTRIBUTOR_ROLE) nonReentrant
    returns (uint256 nextIndex, uint256 totalHolders, uint256 amountDistributed);
```

The mechanics that matter for driving it correctly:

* **The full `totalAmount` is pulled from the caller only on the `startIndex == 0` window** (`safeTransferFrom(msg.sender, this, totalAmount)`). Approve that total to the token before the first window; later windows pay out of the balance already held.
* **`nextIndex` wraps to `0` when the walk completes.** Loop `distributeYieldWithLimit(total, nextIndex, …)` until `nextIndex` comes back `0`.
* **Yield-restricted holders' shares stay in the token contract** — they are skipped, not redistributed.

> **On Rome the practical ceiling is `maxHolders = 1` per transaction** — the same 62-account cap from step 2 — so yield is walked one holder per transaction (measured \~318 K gas per holder). Every whitelisted holder, EVM or Solana-native, receives pro-rata; the reference script is `measure-yield-batch.ts` and the funded smoke's yield step asserts it end-to-end.

```bash
# USAGE.md reference form: walk one holder per tx until nextIndex wraps to 0
distributeYieldWithLimit(total, offset, 1)
```

***

## Step 9 — Verify what you deployed

Nothing auto-submits sources anywhere in Rome, so verification is a step you run. After any deploy — infrastructure or a new token — run the read-only, key-less, idempotent verifier (already-verified contracts are skipped, so re-running after issuing a token verifies just the new ones):

```bash
cd scripts && CHAIN_ID=<id> npx tsx verify-deployments.ts
```

It expects the **three** contracts per token (proxy + both modules) plus the infrastructure, and reports what it could not verify rather than failing a deploy. Doing this by hand once missed seven contracts.

Registering the asset into the Rome registry treats verification as a **gate**, not decoration (`register-asset.ts`): a token earns its `apps/arc/<chain>.json` record — carrying `standard: arc-permissioned-erc20` and a `transferRestriction` naming its allowlist gate — only when all three of its contracts are verified on Sourcify. That record reads the chain for its facts (a wizard-deployed token has no committed receipt) and writes only a local registry checkout; landing the PR is a person's job.

```bash
CHAIN_ID=200010 npx tsx register-asset.ts <tokenAddress>        # verify + prepare the registry record
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # read-only preview
```

For an end-to-end proof of the whole loop — create, whitelist, gate, sell on both lanes, distribute yield — run the funded smoke against any chain:

```bash
PRIVATE_KEY=… CHAIN_ID=… SOLANA_KEYPAIR=… npx tsx smoke.ts
```

***

## Step 10 — Know what stays off-chain

The trust model is deliberately honest, and a builder should represent it the same way to their own users:

* **The allowlist flag is your off-chain decision.** KYC/AML happens in your process — your vendors, your rules. The chain records and enforces only the *outcome*, `isWhitelisted(addr)`.
* **The only on-chain state is one boolean per address.** No PII, no documents, no identity claims, no on-chain attestations, no cryptographic proof of who an address belongs to. This is Arc's model as used on Plume; it trades the heavier transfers of an on-chain-identity standard (e.g. ERC-3643) for lighter mechanics and issuer-held trust.
* **Recovery is an issuer power, not an escape hatch.** A lost wallet — EVM or Solana — is recovered by whitelisting a replacement address and, if needed, using mint/burn/upgrade under your own legal process (Arc has no built-in forced-transfer primitive). The issuer's key custody is therefore part of the compliance posture.
* **No relayer of Bloom's own, no custody surface.** Users hold their own gas on both lanes. A fee-payer service run by the app would mean holding keys — a surface an RWA product should not add. The one relay on the Solana lane is the chain proxy's, for a call too large for one transaction, and the user still authorizes it with their own signature and pays for it in their own Rome gas.

***

## Reference: the tooling

Every script runs from the tooling's `scripts/` directory, with `PRIVATE_KEY`, `CHAIN_ID`, and `REGISTRY_ROOT` exported.

| Script                          | What it does                                                               |
| ------------------------------- | -------------------------------------------------------------------------- |
| `deploy-infra.ts`               | One-time per chain: the four shared contracts, wired for wUSDC.            |
| `create-token.ts`               | The piecewise wizard — eight transactions, one token.                      |
| `rotate-factory.ts`             | Deploy a fresh factory after a compiler/artifact change; re-wire.          |
| `verify-artifact-provenance.ts` | Prove your proxy artifact is accepted by the deployed factory (read-only). |
| `verify-deployments.ts`         | Verify all three contracts per token + infra on Sourcify (read-only).      |
| `register-asset.ts`             | Verify-gate, then prepare the registry compliance record.                  |
| `measure-yield-batch.ts`        | Re-measure the per-tx yield ceiling on a chain.                            |
| `smoke.ts`                      | The funded end-to-end suite across both lanes.                             |
| `sweep-synthetic-native.ts`     | Reclaim native gas from a synthetic account (test hygiene).                |

Deeper background lives alongside this guide: `docs/ARCHITECTURE.md` (the layered app model), `docs/LANES.md` (the two lanes and custody), `docs/COMPLIANCE.md` (the trust model), and `docs/USAGE.md` (the operator runbook).


# Evidence — an issuer

The issuer screenshots in [Issuing](/apps-on-rome/bloom/issuing) are not mock-ups. They are a real asset — **Aurelian Estate Fund (`AURE`)** — issued and administered on Hadrian (chain `200010`) by a real wallet, and **every action behind them is a transaction you can open on the block explorer.** The link is the claim.

* **Asset:** [`0x7399b37aef46439c2e884e0ea1c3205b112a41fc`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x7399b37aef46439c2e884e0ea1c3205b112a41fc)
* **Administered by:** [`0x5667b3700b4a021Cc475157cbf3dE2d756Fae597`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x5667b3700b4a021Cc475157cbf3dE2d756Fae597)
* **Allowlist module:** [`0x4530539c02ad3f4ac519c1bb378cefbbc1666946`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x4530539c02ad3f4ac519c1bb378cefbbc1666946)
* **Yield module:** [`0xea8a7496420b0e65ce07c487ed502aabef489e29`](https://via-hadrian.testnet.romeprotocol.xyz/address/0xea8a7496420b0e65ce07c487ed502aabef489e29)

## Creating the asset — eight transactions, never one

The wizard's piecewise deploy (see [Building on Bloom](/apps-on-rome/bloom/building#step-2-deploy-it-piecewise-never-as-one-transaction)), each contract in its own transaction:

| # | What it did                             | Transaction                                                                                                                                     |
| - | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Deploy the asset (proxy + `initialize`) | [`0x64000cd3…d4ff8e10`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x64000cd37caf44f7a37216d8e2963c082af9655a762b3401c5b7d3fad4ff8e10)     |
| 2 | Deploy the allowlist module             | [`0xe1df119d…58808a8a`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xe1df119d3c9ed31786b825c489d62d3a24d996e86a169c8a8ca79a0658808a8a)     |
| 3 | Set you as its administrator            | [`0xba2cd3ec…4251cf5b56`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xba2cd3ec4f10a9f5170e727c1ed3b4daa0d517303cf925babe0104da6a23681b)   |
| 4 | Enforce it on transfers                 | [`0x8570ded6…3890b8e83d`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x8570ded695c56a26617ebdc4be8c048b6db760b4ea28bf479829623890b8e83d)   |
| 5 | Deploy the yield module                 | [`0x9425f6af…89a64e52f`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x9425f6afc0492d09874e921b2711d21c7676dd060362cab95dbf0e189a64e52f)    |
| 6 | Set you as its administrator            | [`0x614b91be…08497c0b`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x614b91be462cd6d325ec29c794c23b4854d39b3b5c658b09d9e8406708497c0b)     |
| 7 | Enforce it on yield                     | [`0x150b75cf…41cbdff23160`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x150b75cfaf444dabc452fe3ed43e45dac7f96835c8c3277d47c341cbdff23160) |
| 8 | Register with the factory               | [`0xc9e15848…ea2c80dde`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xc9e1584808ca459549c5c35d67fddf815f449b93ef9aace14ec58dfea2c80dde)    |

## Setting up the sale and the allowlist

| Action                | What it did                                                | Transaction                                                                                                                                     |
| --------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Move inventory        | 100 units to the shared storefront                         | [`0xd9918bb9…d819cefe`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xd9918bb95cd79e393bfab67b257847290fddfe5f10f90054456fb7a0d819cefe)     |
| Open the sale         | 100 units at 1 wUSDC                                       | [`0xc14c0b18…ebd1bd329`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xc14c0b1862526fffd5c2bf3f9d313fbd0e9a7f11c5aca7fa58ca5f5ebd1bd329)    |
| Allowlist a **batch** | three addresses in one transaction (`batchAddToWhitelist`) | [`0xedd533de…3e92bac2e560`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xedd533de80ceccdff07d94240cb29231aea6a3443bf4f37fdd943e92bac2e560) |

## Administering the asset

| Action                  | What it did                                | Transaction                                                                                                                                   |
| ----------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Set the payout currency | wUSDC (`setYieldToken`)                    | [`0x17113a81…c8c5a8f3d`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x17113a8114cea002a3201f9086688d208b2d53df8c671c9606e98c2c8c5a8f3d)  |
| Grant a role            | `MANAGER_ROLE` to a delegate (`grantRole`) | [`0x23b790ad…d2c8c0bd91`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x23b790ad9810ef3114a093411e0c46f1e9ca76bf377a9a42169439d2c8c0bd91) |

> **What the screenshots show is what these transactions did.** The console's Sale tab reads the sale opened above; the "Who may hold it" count is the batch allowlist; the Rights tab reads the roles granted at deploy. Nothing in the guide is staged in a design tool — it is one asset, on one chain, and each number traces to a hash here.


# Evidence — an investor

The [Investing](/apps-on-rome/bloom/investing) screenshots are a real buyer, twice over — **once from an EVM wallet and once from a Solana wallet, buying the same Solidity asset** (Mineral Vault I, `ARCV`) on Hadrian (chain `200010`). Every action is a transaction you can open. The link is the claim.

These are **historical runs**: they happened on the lane as it was then, and the transaction links stay as they were. Where the Solana lane has since changed, the section on it says what the run was and points to [LANES.md](/apps-on-rome/bloom/lanes) for how the lane works now.

* **Asset:** [`0x2b13ba16a429bf630f94882e477fe775d34b7c74`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x2b13ba16a429bf630f94882e477fe775d34b7c74)
* **Storefront:** [`0x9326a00dd1cd4f3f1800fc849c3c58715fc5061a`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x9326a00dd1cd4f3f1800fc849c3c58715fc5061a)
* **Paid in:** wUSDC [`0xd4cc34b67c805d472b5a709a22a1037f6b16ef28`](https://via-hadrian.testnet.romeprotocol.xyz/address/0xd4cc34b67c805d472b5a709a22a1037f6b16ef28) — the wUSDC these runs used. The chain's current wUSDC is `0xbce339ba4c3a8de3bb8af1c5539e6ac11da3112d`, as listed in the public registry.

## The EVM lane — a wallet buys at its own address

An EVM wallet holds the asset at its own address, and a buy is the ordinary two-step approve-then-buy. The eligibility gate is checked on the token first — the buyer is allowlisted, then buys.

Buyer: [`0x5667b3700b4a021Cc475157cbf3dE2d756Fae597`](https://via-hadrian.testnet.romeprotocol.xyz/address/0x5667b3700b4a021Cc475157cbf3dE2d756Fae597)

| # | What it did                                                              | Transaction                                                                                                                                 |
| - | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Allowlist the buyer (the test-wiring stand-in for the issuer's approval) | [`0xf8180aef…5c29c86d`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xf8180aef8ba3203750a4ae63eea215c9e5891cd147f7272a359f512c5c29c86d) |
| 2 | Approve wUSDC to the storefront                                          | [`0x0f7229ea…7c2669e5`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0x0f7229eab17d8d28907a3bc9239f62367abed528fde09bd7cc0ad10f7c2669e5) |
| 3 | Buy the asset                                                            | [`0xe099d892…3792f6de`](https://via-hadrian.testnet.romeprotocol.xyz/tx/0xe099d8921225563cf0fbf0e4fd1e97037f50bcbebf41f2acea21696f3792f6de) |

After the buy, the eligibility card reads **Permitted** and the holding shows in Positions — the `invest-permitted` and `positions` screenshots in the guide are this buyer, after these transactions.

## The Solana lane — a wallet buys at its derived address

This is the one that carries the whole thesis: **a Solana wallet buys the same Solidity asset, with no bridge and no second token.** There is no EVM key anywhere — the buyer signs with their Solana key, and the asset is held at the EVM address *derived* from it.

* **Solana signer:** `55R41dbRU13QhLpAgha1841wR5M6sAcZhXd4S1LGupBn`
* **Derived holding address:** [`0xd868eec27e47376e739d0a3555f81b7657faa322`](https://via-hadrian.testnet.romeprotocol.xyz/address/0xd868eec27e47376e739d0a3555f81b7657faa322) — `keccak256(pubkey)[12..32]`

In this run each step was an EVM transaction executed inside **one Solana transaction**, authorised by the Solana signature, and the client built that transaction itself. The buy is the same two legs, each its own Solana transaction:

| # | What it did                                                | Solana transaction                                                                                                                                         | Compute   |
| - | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| 1 | Approve wUSDC to the storefront (from the derived address) | [`3ohASbQc…3ad5p`](https://explorer.solana.com/tx/3ohASbQcv3L7qPbJfccoZQeN1KX1yPBy4nycdSU6PVWD8DxrpfGgHVymbHHfkLWC5727xE3XJTzf1Fyf5ct3ad5p?cluster=devnet) | \~191K CU |
| 2 | Buy the asset                                              | [`5CdSWWh3…3UkJ`](https://explorer.solana.com/tx/5CdSWWh3A72jy2M843F3udYjiHr2UUM82FrfxjcNJja8RZrj6AQH7mHCMmZafgMFBUGBCMriXQiS7jM36rQZ3UkJ?cluster=devnet)  | \~737K CU |

The buyer paid the transaction fee in **lamports from their own Solana wallet**; the derived address never held SOL. That part has not changed.

The `positions-solana` screenshot in the guide — *"Held for your Solana key"*, the custody chain, and the ARCV holding — is this signer, after these transactions.

> **How the lane works now.** These two transactions were built by the client and signed once each. Today the chain's proxy composes a Solana v1 transaction for every call, and the wallet signs twice per call: a readable *Rome authorization* message, then the transaction. A fresh wallet may also see a cash-in step and a one-time *Allow wUSDC transfers* step before the approval, and a call too large for one transaction is relayed. The links above are the earlier lane's transactions, not the current one's. See [LANES.md](/apps-on-rome/bloom/lanes).

> **The same asset, the same allowlist, either wallet world.** The EVM buyer and the Solana buyer bought the identical Solidity token, gated by the identical on-chain allowlist, priced in the identical wUSDC. The only difference is which key signed and which address holds — which is the entire point of the two lanes, proven here rather than asserted. The mechanics are in [LANES.md](/apps-on-rome/bloom/lanes); the compliance model is in [COMPLIANCE.md](/apps-on-rome/bloom/compliance).


# Compliance model

Bloom runs Plume's Arc framework unmodified. Compliance is **enforced by the token contract itself** — not by any off-chain screen, sequencer filter, or venue policy. That is the property that makes it portable: the rules travel with the token to any execution path, on both wallet lanes.

## Enforcement architecture

```
ArcToken._update (every transfer/mint/burn)
   └─ RestrictionsRouter (one per chain, module-type registry)
        ├─ TRANSFER_RESTRICTION → WhitelistRestrictions (per token)
        ├─ YIELD_RESTRICTION    → YieldBlacklistRestrictions (per token)
        └─ GLOBAL_SANCTIONS     → (slot exists; no global module deployed)
```

* **WhitelistRestrictions** — the allowlist. While `transfersAllowed` is false (gated mode), a transfer where either side is off the list reverts with the typed error `TransferRestricted()` (`0xe827105e`). The check runs inside the token on every balance movement — an EVM-wallet transfer, a Solana-signed transfer, a storefront sale, and the initial liquidity move all pass through the same gate. There is no path around it short of the issuer's own admin powers.
* **YieldBlacklistRestrictions** — excludes addresses from yield distributions; their pro-rata share stays in the token contract.
* **GLOBAL\_SANCTIONS** — the router supports chain-wide modules applied to every token (e.g. a sanctions list). None is deployed today; the slot is the natural home for one operated at the chain level.

## The trust model, stated honestly

**The allowlist flag is the issuer's off-chain decision.** KYC/AML happens in the issuer's process (their vendors, their rules); the chain records and enforces the *outcome* — `isWhitelisted(addr)`. [Onboarding and review](/apps-on-rome/bloom) is the mechanism around that decision — how an investor applies, how the issuer decides, and the audit trail it leaves — and it does not change this model: still the issuer's call, still only a boolean on-chain, and no documents anywhere. This is the Arc framework's own model as used on Plume. It is deliberately *not* an on-chain identity system: there are no identity claims, no on-chain attestations, no cryptographic proof of who an address belongs to. Standards that carry verifiable on-chain identity (e.g. ERC-3643) trade significantly heavier transfers for that property; Arc trades lighter mechanics for issuer-held trust. Bloom surfaces this honestly rather than implying more.

## Issuer powers (transfer-agent shaped)

`ArcToken.initialize` grants the wizard driver every role:

| Role                                            | Power                                                                                    |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `ADMIN_ROLE` / `DEFAULT_ADMIN_ROLE`             | link/replace restriction modules, manage roles                                           |
| `MINTER_ROLE` / `BURNER_ROLE`                   | supply management                                                                        |
| `YIELD_MANAGER_ROLE` / `YIELD_DISTRIBUTOR_ROLE` | set the yield token, run distributions                                                   |
| `UPGRADER_ROLE`                                 | UUPS-upgrade the token (issuer-held; grant to the factory for factory-mediated upgrades) |
| Whitelist module `WHITELIST_ADMIN_ROLE`         | add/remove addresses, toggle gated mode                                                  |

Consequences worth knowing: a **lost investor wallet** is recoverable only via issuer action (whitelist a replacement address and, if needed, use mint/burn/upgrade powers per their legal process — Arc has no built-in forced-transfer primitive). The issuer's key custody is therefore part of the compliance posture; the app's role viewer makes the holders of each power visible.

## What Rome adds — and what registerToken preserves

Solana-wallet users appear as **synthetic EVM addresses** (derived from their Solana public key). To the compliance layer they are ordinary addresses: the issuer whitelists them, the gate checks them, yield reaches them — one allowlist spans both wallet worlds. Custody implications are in [LANES.md](/apps-on-rome/bloom/lanes).

Because Rome caps accounts per transaction, tokens deploy piecewise and gain factory status via `ArcTokenFactoryV2.registerToken` instead of the one-shot `createToken`. The guards preserve the upstream security model exactly: the token must be a canonical proxy (codehash), its implementation must be factory-whitelisted (codehash), the caller must hold the token's `ADMIN_ROLE`, and registration is once-only. Nothing about the compliance surface is weakened by the piecewise path.


# The two lanes

One token deployment, two wallet worlds. Both lanes execute the **same contracts** with the **same compliance gate** — they differ only in who signs and how the transaction reaches the chain.

## EVM lane

Standard Ethereum tooling, unchanged: the wallet signs an EVM transaction, the chain's JSON-RPC endpoint accepts it, the token contract enforces the allowlist. Nothing Bloom-specific to integrate.

Rome gas is USDC-denominated, and a write costs more than its Ethereum equivalent whenever it has to create Solana accounts (a first-time holder, a new token account). Use `eth_estimateGas`, as standard tooling does, and never hardcode 21,000; the estimate includes the account creation, and Rome charges the gas actually used. A gated transfer also pays for the allowlist check, so it costs more than a plain transfer of the same size.

## Solana lane

A Solana wallet drives the same EVM contracts **natively — no EVM key exists anywhere**. Every call is a Solana v1 transaction that the chain's proxy composes for the wallet to sign. There are no address lookup tables and no compute-budget instructions to build: the proxy sets the compute and heap budget itself.

1. **Identity.** The user's EVM-side address is *synthetic*: derived deterministically as `keccak256(solana_pubkey)[12..32]`. The Rome EVM program derives it on-chain from the transaction's actual signer — it cannot be spoofed, and no secp256k1 private key for it exists, so **the Solana signature is the only thing that can ever drive this address**.
2. **Authorize.** The app builds the call as an unsigned EIP-1559 payload, using the nonce, gas price and gas estimate it reads from the chain. The wallet signs a readable message — it shows as *Rome authorization* followed by a hash of the call — which proves the Solana key authorizes this exact EVM payload. This is the first prompt, and the proxy will not compose the call without it.
3. **Compose.** The app sends the payload and that signature to the proxy. The proxy checks the signature, emulates the call to find which Solana accounts it touches and which are writable, and returns one Solana v1 transaction. The app checks the returned message before it asks the wallet to sign it.
4. **Sign and submit.** The wallet signs the transaction itself: the second prompt, a different signature over a different byte string. The app sends the signed transaction straight to a Solana RPC; the user pays the Solana fee in SOL from their own wallet.
5. **Confirm.** The transaction confirms on Solana; the EVM view catches up a beat later, so the app waits for the synthetic address's nonce to move before it sends the next call.

**The wallet has to sign Solana v1 transactions.** The lane fails closed without that: a wallet that cannot sign a v1 transaction stops the call with an error, and the app never falls back to an older transaction format. Solflare signs v1 on devnet today; Phantom does not yet.

### Before the call: cash and the one-time grant

Two things can come before a Solana-lane call, and each appears as its own step with its own prompts:

* **Moving cash in.** A call that spends wUSDC pulls it from the synthetic address, which holds nothing at rest. If the synthetic holds less than the call needs, the app first moves the shortfall from the wallet's own USDC token account to the synthetic's. This is one Solana transaction signed with one prompt, not an EVM call: an idempotent create of the synthetic's token account, then the Rome program's activate-token-account instruction, which moves the tokens without running the EVM. It is skipped when the synthetic already holds enough.
* **The one-time wrapper grant.** wUSDC is the wrapper of an SPL token, and a wrapper moves a user's tokens as that user's SPL delegate. The first time a wallet's synthetic address spends wUSDC, the grant is not yet in place, and the app puts an **Allow wUSDC transfers** step ahead of the rest. It is one call like any other (two prompts), once per wallet. See [One-time grant per wallet](/core-concepts/token-interop#one-time-grant-per-wallet).

A buy from a fresh Solana wallet is therefore a short sequence: move cash in, allow wUSDC transfers, approve the storefront, buy.

### Calls too large for one transaction

An atomic Solana transaction has fixed ceilings: about 1.4M compute units, 64 accounts and 4 KB on the wire (see [Constraints](/core-concepts/constraints)). Bloom always tries the direct, wallet-signed transaction first. Only when the proxy refuses the call because it does not fit one transaction does the app hand it to the proxy to **relay**, and never before:

* **One prompt for that call.** The relay reuses the authorization message the wallet already signed for the refused attempt, so there is no second identical prompt and no transaction to sign. The proxy composes the call, submits it to Solana (in as many transactions as it needs) and pays the Solana fees itself.
* **Rome gas comes from the synthetic's balance.** A relayed call is paid in the chain's native gas, in wei, from the synthetic address. If that balance is short of the call's cost, the app first tops it up from the wallet, in whole units of 10^12 wei: one more prompt, normally once per wallet, like the grant.
* **The hash is a Rome transaction hash.** The result of a relayed call is the proxy's EVM transaction hash, not a Solana signature, so it will not open on a Solana explorer; look it up on the chain's own explorer.

The relay applies to whatever call the proxy reports as too large for one transaction, which in Credit can include the larger market actions. Most calls, including a plain transfer and an approval, fit one transaction and never take this route. The mechanics of relayed calls and their limits are in [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls).

### Prompts per step

| Step                                                      | Wallet prompts                                                |
| --------------------------------------------------------- | ------------------------------------------------------------- |
| Move cash in (only if short)                              | 1                                                             |
| Allow wUSDC transfers (once per wallet)                   | 2                                                             |
| A call that fits one transaction (approve, buy, transfer) | 2: the authorization message, then the transaction            |
| A call that has to be relayed                             | 1, plus 1 for the one-time gas top-up if the balance is short |

On the Invest asset page, a Solana wallet goes through the cash-in, the grant, the approval and the buy in one run after pressing Buy, and the button notes "two wallet prompts" while each step signs. The named steps ("Transferring your cash") and the up-front prompt counts are shown on Trade and Credit. An EVM wallet also gets a one-time *Allow wUSDC transfers* step on the buy panel.

### Measured compute, from the earlier lane

Before the move to v1 transactions, the lane built its own transaction and measured these figures on Rome devnet, ranges observed across two chains. They have not been re-measured on the v1 transaction the proxy composes now, so read them as an order of magnitude, not a budget:

| Solana-lane flow                                                | CU         |
| --------------------------------------------------------------- | ---------- |
| Gated RWA transfer                                              | \~339–381K |
| wUSDC approve                                                   | \~189–191K |
| Storefront buy (approve + buy, two transactions)                | \~730–796K |
| Cash-leg sweep to the wallet's own token account                | \~42–54K   |
| Plain value transfer                                            | \~47K      |
| `TestApprover.approve` — allowlist admission, **wallet-signed** | 220,794    |

Solana's per-transaction compute ceiling is 1.4M CU. Measure your own call from the Solana transaction's `meta.computeUnitsConsumed` (`getTransaction` on a Solana RPC; the chain's EVM receipt has no such field) rather than assuming one of these.

## Custody: why the RWA stays at the synthetic

The design question every permissioned asset must answer: *can the token leak out of the compliance perimeter?* On the Solana lane the answer is structural, not policy:

* **The RWA is EVM-state-resident.** Its balances are storage inside the Rome EVM program's accounts. There is **no SPL mint** of it, no token account representation, nothing Solana-native to move. (This is deliberate — a wrapped/mirrored version would be the escape hatch.)
* **The Solana-side plumbing can't touch it.** The lane's asset-moving legs (`create_ata`, SPL transfers, sweeps) operate on SPL token accounts; EVM storage balances have no such account. There is no instruction that moves an EVM balance to a Solana token account.
* **Only the owner's Solana signature drives the synthetic**, and every movement it can express is a call into the token contract — which runs the allowlist gate — so a transfer to a non-whitelisted address reverts identically whether signed by an EVM key or a Solana key.
* **The cash leg is the deliberate exception.** wUSDC (yield, sale proceeds) is SPL-backed and *should* round-trip: the exit leg (`transfer_spl`) moves it from the synthetic to the wallet's own associated token account, verified end-to-end. Asset stays in the perimeter; cash flows freely.

Two practical consequences, stated plainly: a Solana wallet UI won't display the RWA holding (there is no SPL token to list) — the Bloom app is the positions surface; and a lost Solana key is recovered through the issuer's process ([COMPLIANCE.md](/apps-on-rome/bloom/compliance#issuer-powers-transfer-agent-shaped)), not by moving the asset out-of-band — which is exactly the property a permissioned asset wants.


# Privacy DEX

Private swaps, a USDC vault, and limit orders on Solana devnet.

Privacy DEX swaps tokens on Solana and puts the tokens you receive in your Helius Privacy private balance. A **private balance** holds encrypted records called **notes**, each containing a token and an amount. The app decrypts your notes in the browser so you can see and use your balance. Your wallet signs the transactions that spend it.

Rome's **market maker** supplies the tokens you trade for and offers **quotes**: signed terms for a swap, including its amounts, fee, and expiry. Rome's escrow program checks the signed quote and the proofs before settling the trade on Solana.

Open [Privacy DEX on Solana devnet](https://privacy-dex.devnet.romeprotocol.xyz). Helius Privacy is in beta. This release runs on Solana devnet only.

## What you can do

A market swap on the swap card exchanges USDC for SOL on Solana devnet. Choose **Wallet** to pay public USDC and receive private SOL. Choose **Private balance** to pay private USDC and receive private SOL. The card's **Limit price** option places orders in either direction.

You can also:

* Add tokens to your private balance without swapping them.
* Move funds to a wallet or send them to another person.
* Review your private activity and swap attempts.
* Put USDC into a vault in exchange for share tokens, which you can exchange back for USDC. Their value grows at 2% a year, with simple growth from the vault's start. Rome sets this rate and pays for it on devnet. The rate is not enforced on chain, and Rome can change it.
* Place a limit order in either direction of a pair and cancel its unfilled part.

## Connect with Solflare

Use **Solflare** for this release. It supports the version 1 transactions used by Privacy DEX on Solana devnet. It also signs the message used to derive your private account's keys.

Phantom is not supported in this release because it does not sign version 1 transactions on Solana devnet. The app requires that transaction version when connecting a wallet.

## Choose a guide

* [Swaps and the private balance](/apps-on-rome/privacy-dex/swaps) covers quotes, wallet approvals, funding, sending, activity, unlocking, and backup.
* [Vaults](/apps-on-rome/privacy-dex/vaults) explains vUSDC, the rate, and putting in or taking out USDC.
* [Limit orders](/apps-on-rome/privacy-dex/limit-orders) covers placing, matching, fees, and cancellation.
* [What is private](/apps-on-rome/privacy-dex/what-is-private) explains what the chain and services see.

Your address remains visible even when the amount is private. Paying from a wallet exposes the token and amount. Read the privacy guide before choosing how to fund a trade.


# Swaps and the private balance

Swap with Rome's market maker and manage your private balance.

A market swap on the swap card exchanges USDC for SOL on Solana devnet. Its [**Limit price** option](/apps-on-rome/privacy-dex/limit-orders) places orders in either direction.

Your **private balance** holds encrypted records called **notes**, each containing a token and an amount. Before swapping, connect Solflare and [set up or unlock your private account](#unlock-and-back-up). Select **Market** on the swap card and enter the USDC amount you want to pay. The **From** switch chooses whether the USDC comes from your wallet or your private balance. The SOL you receive goes into your private balance in either case. Settings lets you choose a default payment source.

## Quotes and approval

The displayed market price is an estimate. Rome's **market maker** supplies the tokens you trade for. Select **Swap** to request a **firm quote**: signed terms for your swap, including the amounts, fee, and expiry. The maker reserves funds for that quote. The browser checks those terms against the trade shown to you. Review the quote, then select **Swap** again to approve the trade in Solflare.

A firm quote lasts **180 slots**, about **43 seconds on Solana devnet**. Slots are Solana's units of time. The countdown uses the quote's expiry slot and the measured slot time. It ends a few seconds before the quote expires so the app does not ask you to sign a price that is about to run out. If the quote expires, select **Get new price** before trying again.

Solflare asks you to sign the transaction that pays for and settles the swap. Larger trades can require the maker's final approval. The **Price** row then says **Checked again when you swap**. For those quotes, you sign first, and the maker re-checks the price. It then co-signs and submits the transaction or declines it.

Rome's escrow program checks the quote and a proof that the trade follows its terms. Helius Privacy checks a separate proof that the private transaction is valid and updates the notes. The app confirms the transaction and decrypts the received note before showing the swap as filled.

## Pay from your wallet

An **entry swap** pays USDC directly from your connected wallet. You do not need to add that USDC to your private balance first. The payment and the private SOL receipt settle together in one transaction.

Your wallet address, the USDC, and the payment amount are public on Solana. The received note is encrypted, but the available pair identifies SOL as the receive token, and the public payment helps observers estimate its amount.

## Pay from your private balance

A **private swap** spends private USDC and receives private SOL. Neither amount is published as a public payment or withdrawal. Your address and the maker's address remain visible.

The swap needs a private note containing exactly the amount you pay. If necessary, the app first prepares that note with a private transfer to your own account, before requesting the firm quote. That preparation can require a separate wallet approval.

If settlement fails, the swap's token transfers are reversed. The network fee can still be charged. If confirmation is uncertain, check your balances and the transaction before trying again.

## Add, move, and send

**Add to private balance** deposits a token from your wallet without exchanging it. Your address, the token, and the amount are public at this step.

**Move to wallet** withdraws to a public wallet address. The destination defaults to your connected wallet. The destination, token, and withdrawn amount are public.

**Send** transfers privately to a recipient with a registered private account. The sender and recipient addresses are visible, while the token and amount are encrypted. Sending to an unregistered recipient requires separate confirmation of a public withdrawal that reveals the token and amount.

These actions use Helius Privacy directly and do not need a maker quote. Your wallet signs each transaction. Each confirmation links to the transaction in the Solana explorer.

## Activity

Activity combines your decrypted transaction history with your swap attempts. It labels added funds, swaps, vault put-ins and take-outs, limit orders placed, filled and cancelled, sends, receipts, withdrawals, and unsuccessful attempts. Use the transaction link to check the result on Solana.

## Unlock and back up

Connect Solflare and set up or unlock your private account. Signing the unlock message derives your private account's keys in your browser. The viewing key lets you read your private balance and activity. The nullifier key lets you recognise when your notes have been spent. Signing this message spends no funds, but anyone with the signature can read your private balance and activity. Only sign it on the Privacy DEX site. First-time setup also asks you to publish your private account's public keys in a registration transaction, which costs a network fee. Unlocking returns you to the page you were using.

The app's sign-in message is separate from the key-derivation message. It establishes a session and does not authorize a payment. Settings includes **Lock** and controls to show or restore a backup.

The optional **48-word backup** restores your viewing and nullifier keys. Anyone with these words can see your private balance and activity. Keep them as private as your wallet's recovery phrase. It does not replace your wallet or authorize spending without it. The same wallet can derive the private keys again when you unlock. Keep your wallet's recovery phrase safe: losing access to that wallet means losing access to the funds, and Rome cannot recover them for you.

See [What is private](/apps-on-rome/privacy-dex/what-is-private) for what Rome and Helius learn during these actions.


# Vaults

Put USDC into a private vault with a rate set and funded by Rome on devnet.

The **USDC vault** exchanges USDC for its share token, **vUSDC**. Shares are tokens you can exchange back for USDC through the vault. You hold them in your **private balance** as encrypted records called **notes**. Their value in USDC grows at **2% a year** on Solana devnet. This is simple growth from the vault's start, without compounding. Rome sets this rate and pays for it with devnet tokens. The rate is not enforced on chain, and Rome can change it. The devnet vault does not lend your funds. Its rate follows a published schedule rather than returns from lending.

## Your vault balance

The vault page shows what your shares are worth in USDC at the current rate. You do not need to make a transaction for your shares to grow in value. Each share becomes worth more USDC; you do not receive a separate payment.

Putting in exchanges USDC for shares at the vault's quoted rate. Taking out exchanges shares back for USDC. The quote gives the amounts you will exchange. The app checks it against the rate schedule published by Rome's server before asking you to sign.

## Put in

Connect Solflare and [set up or unlock your private account](/apps-on-rome/privacy-dex/swaps#unlock-and-back-up).

1. Open **Vaults** and choose the USDC vault.
2. Under **Put in**, choose **Wallet** or **Private balance** as the source.
3. Enter a USDC amount up to the 10 USDC cap.
4. Select **Put in** to get a quote. The card shows how long the quote lasts. Select **Put in** again and approve the transaction in Solflare.

From the wallet, the USDC payment is public and the received shares are private. An observer can see that your address put USDC into the vault. The public payment and published rate let them work out the shares received.

From the private balance, the amount stays encrypted. Your address and the vault's address are still visible. The app may first prepare a note containing exactly the amount you pay, as for a [private swap](/apps-on-rome/privacy-dex/swaps#pay-from-your-private-balance). This can need a separate wallet approval.

## Take out

Open **Vaults** and choose the USDC vault. Under **Take out**, enter the USDC amount you want to receive. The app works out how many of your private shares to exchange. Select **Take out** to request a quote and review the quoted amount. Then select **Take out** again and approve it in Solflare. As with a private swap, the app may first prepare an exact-amount note, which can need a separate approval. The USDC arrives in your **private balance**.

To receive it in a public wallet, follow with **Move to wallet**. That separate withdrawal reveals the destination, token, and amount on Solana.

**All** selects as much of your vault balance as the per-action cap permits. A balance above the cap takes more than one action to withdraw.

## Devnet caps and availability

Each put-in is capped at **10 USDC**. Each take-out is capped at **10 USDC**. These are limits on individual actions.

Each action must be covered by one available note holding the vault's funds. If no note covers the requested amount, the app asks you to try a smaller amount. When the vault is unavailable, its action buttons are disabled. Taking out depends on Rome's vault service being available and holding enough to cover your amount. Rome keeps the devnet vault funded. There is no other way to exchange shares back for USDC.

Vault trades settle through Rome's escrow program, as [swaps](/apps-on-rome/privacy-dex/swaps) do. Put-ins can pay from either source; take-outs always spend shares from your private balance. Rome sees their quoted amounts, and Helius's hosted prover sees the proof inputs. See [What is private](/apps-on-rome/privacy-dex/what-is-private) for the full visibility rules.


# Limit orders

Place private limit orders, understand matching and fees, and cancel unfilled funds.

A limit order waits for another order that can trade within its price limit. Rome's **matcher** is the service that keeps the private list of waiting orders, called the **order book**, and pairs orders that can trade with each other. There is no public list of the orders' tokens, amounts, and prices. The matcher can read those terms.

Your **private balance** holds encrypted records called **notes**, each containing a token and an amount.

## Place an order

Connect Solflare and [set up or unlock your private account](/apps-on-rome/privacy-dex/swaps#unlock-and-back-up). Select **Limit price** on the swap card. Choose the pair's direction, the token amount to pay, and **Wallet** or **Private balance** as the payment source. You can place an order in either direction of a pair.

The limit price starts at the current market price for the chosen direction. You can edit it or select **Use market price** to reset it. Changing direction resets the limit to the market price for that direction. The price field shows how much of the payment token one receive token costs.

Set **Order lasts**, review the terms, and select **Place order**. Your wallet signs the placement. Paying from **Private balance** takes one approval. Paying from **Wallet** takes two: the first adds the exact amount to your private balance as a public deposit, and the second places the order from there. If you refuse the second approval or it fails, no order is placed. Your funds stay in your private balance. You can place the order from there. If the app cannot confirm whether the order was placed, check **Your orders** before trying again.

Placing an order moves its funds into a note owned by an address dedicated to that order. The terms you sign set the limits for trading those funds.

Paying from **Private balance** keeps the placed token and amount encrypted. Paying from **Wallet** makes a public deposit, exposing its token and amount. The price is hidden from the chain in both cases. Your address, the order's address, and the fact that you placed an order are visible.

## How matching works

The matcher considers the **best price first**, then the **oldest order** at that price. A partly filled order keeps its original place in the queue. Its unfilled funds remain at the same order address.

The **resting order** is the one already waiting in the book. The matcher uses its limit as the trade price, with rounding in its favour. A match must satisfy both orders' limits, minimum fills, and expiry. The app sets each order's minimum fill to a tenth of its amount. No single fill can be smaller, unless it takes everything left in the order. An expired order cannot match, and its funds are not returned automatically. They stay at the order's address until you cancel it.

Each match includes a proof checked by Rome's escrow program. The proof checks that each order gets at least its own limit and meets its minimum fill. It also checks the fee's published rate and destination. It does not enforce queue priority, the trade price within the two limits, or which order counts as resting and which pays the fee. Those are Rome's matcher rules. You rely on Rome to apply them and keep matching available.

## The book fee

The **incoming order** is the one matched against an order already waiting in the book. The matcher identifies which order is incoming. The book charges **0.1%**, taken from that order's proceeds in the token it receives.

The paying order's limit is checked **after the fee**. The amount it receives must still meet the minimum promised by its limit. A match that would fall below that limit after the fee cannot settle.

## Cancel an order

Find the order under **Your orders** below the swap card. Select **Cancel** and approve the cancellation in Solflare.

Only the order's owner can cancel it. Cancellation returns the remaining funds to the owner's private balance. It requires the owner's wallet signature, not permission from the matcher. A trading pause does not stop cancellation.

You can cancel an open, partly filled, or expired order to get its remaining funds back in your private balance. The order address links the cancellation to its original placement on chain. It also shows that the order still held unfilled funds when cancelled.

## What a match shows on chain

A match links both order addresses to their placements and shows the traders, the fee recipient, both expiry slots, and Rome's matcher as the fee payer. Each trader receives private proceeds.

The transaction also creates a remainder note at one order's address. Observers can tell that the other order was completely filled. The remainder note can contain zero, so its presence does not reveal whether its order is still open.

When both orders were paid from private balances, their amounts and trade price stay encrypted. A wallet-funded placement exposes information that can carry through to the match: if that order is completely filled, observers can work out what its counterparty received, allowing for the fee. If both wallet-funded orders are completely filled, their public amounts also reveal the trade price.

[What is private](/apps-on-rome/privacy-dex/what-is-private) explains what Rome and Helius can see and what you rely on them to do.


# What is private

What Solana observers, Rome, and Helius can see when you use Privacy DEX.

Your **private balance** holds encrypted records called **notes**, each containing a token and an amount. Privacy DEX encrypts those details. It does not hide the addresses involved in transactions. Your browser decrypts your notes so you can see your balance and activity.

The table describes visibility to someone watching Solana. Information hidden from the chain can still be visible to the services that quote, match, or prove a transaction. Rome's **market maker** offers signed swap terms called **quotes** and supplies the tokens you trade for. Its **matcher** pairs limit orders. Helius's **hosted prover** builds proofs that private transactions are valid.

Vault **shares** are tokens you hold in your private balance and can exchange back for USDC through the vault.

| Action                                  | What an observer sees on chain                                                                                                           | What stays hidden from the chain                                                                      |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Set up a private account                | Your wallet address and registered public keys.                                                                                          | Your private keys and private balance.                                                                |
| Add to private balance                  | Your address, the deposited token, and its amount.                                                                                       | The contents of later private notes.                                                                  |
| Swap from the wallet                    | Your address, the payment token and amount, the maker, and the addresses that own the received notes.                                    | The received note's contents, though the pair and public payment let observers estimate them.         |
| Swap from the private balance           | Your address, the maker, and the addresses that own the received notes.                                                                  | Both amounts and the note contents. With one pair, observers can still work out the tokens.           |
| Send privately                          | The sender and recipient addresses.                                                                                                      | The token and amount.                                                                                 |
| Move to wallet or send publicly         | Your address, the destination, token, and withdrawn amount.                                                                              | Your other private notes.                                                                             |
| Put in a vault from the wallet          | Your address, the USDC amount, and that it went into the vault.                                                                          | The share note's contents, but the shares received can be calculated from the public amount and rate. |
| Put in or take out of a vault privately | Your address, the vault, and the addresses that own the received notes.                                                                  | The amounts and whether the trade put money in or took it out.                                        |
| Hold vault shares                       | No transaction as the share rate rises.                                                                                                  | Your share balance and its value, except what earlier public payments allow an observer to infer.     |
| Place an order privately                | Your address, the order address (which links later matches or cancellation to this placement), and that the transaction placed an order. | The token, amount, price, and expiry at placement.                                                    |
| Place an order from the wallet          | Your address, the deposited token and amount, the order address, and that the transaction placed an order.                               | The price and expiry at placement. With one pair, the payment token identifies the token you receive. |
| Cancel an order                         | Your address, the order, and that it still held unfilled funds.                                                                          | The price and remaining note contents, except what paying from the wallet already exposed.            |

## What public payments reveal

An **entry swap** pays from your wallet and makes a public USDC payment. With USDC to SOL as the available market swap pair, an observer can identify the receive token and estimate the amount using the payment and market price. Encryption of the received note does not remove that information.

Sending to someone without a registered private account is a public withdrawal. The app asks for separate confirmation before doing this. Timing and amounts can also link a public deposit to a later withdrawal.

## Order matches

A match reveals both order addresses, both traders, the fee recipient, and both orders' expiry slots. The order addresses link the match back to the original placements.

One order receives a remainder note; the other is visibly completely filled. The remainder can be zero, so observers cannot tell from that note alone whether its order is still open. The amounts and price stay hidden when both orders were funded privately.

If a completely filled order was funded from a wallet, its public payment lets observers work out what the counterparty received, allowing for the fee. If both orders were funded from wallets and both fill completely, their public amounts reveal the price too.

## What Rome learns

Rome records the pair and amounts of each firm quote against your account. The maker knows which funds it reserved for that quote. For a private swap, it also learns which private note pays for the trade. It does not learn your keys or your other private balances from that swap. Rome also sees the tokens and amounts in vault quotes.

Rome's matcher sees every order's token, amount, and price. It chooses matches and sets a price within the signed limits. It cannot cancel your order, and it cannot move your order's funds outside the signed terms without a valid proof. On devnet, Rome generated the proving keys alone, so whoever ran that setup could forge a match and take order funds. A multi-party setup comes before mainnet. You rely on Rome to apply its matching rules and to keep matching available. You also rely on Rome not to trade against your orders using what it sees. On devnet, Rome places no orders of its own.

## What Helius learns

Helius's hosted prover sees the tokens, amounts, and note details supplied to build a transaction proof. Whenever you spend your own private notes, it also receives your nullifier secret, which lets it recognise later spends of your private funds. This includes private swaps, vault put-ins from the private balance, vault take-outs, limit-order placements, sends, withdrawals, and the step that prepares an exact-amount note. A limit order paid from **Wallet** spends private notes in its second step. That secret does not authorize spending; spending your own notes still requires your wallet's signature.

## Keys and trust

This is a test release on **Solana devnet**. **Helius Privacy is in beta**.

Your Solana spending key stays in your wallet. The app derives viewing and nullifier keys in browser memory when you unlock. It stores an encrypted note snapshot and decrypts your balance locally. Rome never holds your keys; the hosted prover receives the secret described above.

A compromised page could obtain your unlock signature and expose your private balance and activity. It could also ask you to sign a harmful transaction. Rome can also upgrade the escrow program. Swaps depend on the maker's proof service even after it has issued a quote.

The [backup](/apps-on-rome/privacy-dex/swaps#unlock-and-back-up) restores private-account keys, but spending still requires access to the original wallet.


# Execution Model

How Rome EVM processes Ethereum transactions on Solana — proxy emulation, atomic (VmAt) vs. iterative (VmIt) modes, and the full transaction lifecycle.

Rome EVM executes Solidity bytecode inside a Solana on-chain program. This page explains how EVM transactions are processed.

## Transaction Lifecycle

```
1. User signs EVM transaction (MetaMask / ethers.js)
                    ↓
2. Rome Proxy receives via eth_sendRawTransaction
                    ↓
3. Proxy emulates transaction off-chain (Mollusk SVM emulator)
   → Estimates gas, checks atomicity, identifies required accounts
                    ↓
4. Proxy wraps EVM tx as Solana instruction(s)
   → If tx fits in one Solana tx → Atomic (VmAt)
   → If tx exceeds CU budget → Iterative (VmIt)
                    ↓
5. Solana validator executes the instruction(s)
   → Rome EVM program interprets EVM bytecode
   → CPI calls to other Solana programs (if any)
                    ↓
6. State changes committed to Solana accounts
                    ↓
7. Hercules indexes the event → produces EVM block
```

## Atomic Execution (VmAt)

The default mode. The entire EVM transaction executes within a single Solana transaction.

**State machine:** `Lock → Init → Execute → Commit → GasTransfer → Exit`

**Properties:**

* All-or-nothing execution — if any step fails, the entire transaction reverts
* \~1.4M compute units available per Solana transaction
* Suitable for transfers, simple contract calls, swaps, most DeFi operations
* Sub-second finality (Solana block time)

**When used:** Automatically selected when the emulator determines the transaction fits within a single Solana transaction's compute budget.

## Iterative Execution (VmIt)

For compute-intensive operations that exceed a single transaction's budget. The EVM execution is split across multiple Solana transactions.

**How it works:**

1. Each step (a Solana transaction) executes **as many EVM opcodes as fit in its compute budget** — an adaptive step size, not a fixed count
2. After each step, the VM state is serialized (Borsh format) into a `StateHolder` account
3. The next step deserializes state and continues execution
4. Accounts involved are TTL-locked for **3-4 seconds** during multi-step execution; a session is capped at **160 steps**; past that, the next step releases the session's locks and ends it without committing anything

**State machine:** `FromStateHolder → Lock → Init → Execute → Serialize → NextIteration → ... → Completed`

**Account locking:**

* One **exclusive lock** per managed account (balance and storage accounts) — only one iterative session can hold an account at a time; an atomic transaction that writes a locked account is refused with `AccountLocked` until the lock lapses, and the client resends
* **TTL:** 3 seconds, renewed every step (4 seconds when the session touches more than 28 accounts)
* **Legacy-track CPIs are refused** inside an iterative transaction (`CpiProhibitedInIterativeTx`); cached-track effects are staged and committed at the end

**When used:** large contract deployments, deep call stacks, any operation exceeding \~1.4M CU. A BN254 pairing check on its own does not need it; see [Compute Budget](/core-concepts/compute-budget).

## Emulation

Before submitting a transaction to Solana, the Proxy emulates it off-chain using the **Mollusk SVM emulator**. This:

1. Estimates gas consumption
2. Determines if atomic or iterative mode is needed
3. Identifies all Solana accounts the transaction will touch
4. Validates the transaction won't fail on-chain

The emulator executes the same EVM logic as the on-chain program — the `entrypoint!` macro ensures identical dispatch tables in both the program and emulator codebases.

**Mollusk SVM** can also execute arbitrary Solana BPF programs during emulation, which means `eth_call` and `eth_estimateGas` correctly handle CPI calls to SPL Token, Jupiter, Kamino, etc.

## Account Mapping

Every Ethereum address maps to a Solana PDA:

```
Ethereum address (H160, 20 bytes)
    ↓
PDA = findProgramAddress(
    [chain_id, "ACCOUN_SEED", H160, bump],
    ROME_EVM_PROGRAM_ID
)
    ↓
Solana account (Pubkey, 32 bytes)
```

**Account types stored on-chain:**

| Type        | Seeds                                        | Purpose                                  |
| ----------- | -------------------------------------------- | ---------------------------------------- |
| Balance     | `[chain, "ACCOUN_SEED", H160, bump]`         | Nonce, balance, contract code            |
| Storage     | `[chain, "STORAGE", H160, slot_index, bump]` | Contract storage (256 slots per account) |
| TxHolder    | `[signer, "TX_HOLDER_SEED", index, bump]`    | Staged transaction data (max 80 KB)      |
| StateHolder | `[signer, "STATE_HOLDER_SEED", index, bump]` | Serialized VM state between iterations   |

## Holder Accounts

Rome sends Solana **v1 transactions**, which are limited to 4 KB (4,096 bytes) and 64 addresses. EVM transactions — especially contract deployments — can be much larger.

**Splitting mechanism:**

1. The SDK splits the RLP-encoded transaction into chunks
2. Each chunk is written to a `TxHolder` account via `TransmitTx` instructions
3. Once all chunks are staged, a `DoTxHolder` instruction assembles and executes the full transaction
4. Maximum holder size: **80 KB** per TxHolder

This is completely transparent to the developer — the Rome SDK handles splitting and reassembly automatically.

## Supported Transaction Types

| Type              | EIP       | Description                                                                                                                                                                                      |
| ----------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Legacy            | —         | Traditional Ethereum transactions                                                                                                                                                                |
| Access List       | EIP-2930  | Optimized state access patterns                                                                                                                                                                  |
| Dynamic Fee       | EIP-1559  | Base fee + priority fee                                                                                                                                                                          |
| Unsigned EIP-1559 | —         | A `0x02`-typed body with no secp256k1 signature, authorised by a Solana wallet's Ed25519 signature instead (the Solana lane; see [Call EVM from Solana](/developer-guides/call-evm-from-solana)) |
| Deposit           | Type 0x7E | Deposit transactions, e.g. bridge inbound settlement                                                                                                                                             |

## Journaled State

Rome EVM uses a journaled state model for managing state changes during execution:

* All changes (nonce, balance, storage, code) are tracked in a `Journal`
* Nested CALL/CREATE operations push snapshot frames
* On revert: journal entries roll back to the snapshot
* On success: changes are committed to Solana accounts
* Non-EVM CPI instructions execute immediately (Solana's own atomicity guarantees correctness)

## What's Next

* [Compute Budget](/core-concepts/compute-budget) — CU costs and optimization strategies
* [Constraints](/core-concepts/constraints) — important limits and boundaries


# Compute Budget

Solana compute unit (CU) costs for Rome EVM operations — atomic vs. iterative budgets and how to design CU-efficient Solidity contracts.

Every Solana transaction has a compute budget measured in Compute Units (CU). Understanding CU costs helps you design efficient Rome contracts.

## Budget Overview

| Mode             | Max CU                                                           | Notes                                                                                         |
| ---------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Atomic (VmAt)    | \~1,400,000 CU                                                   | Single Solana transaction                                                                     |
| Iterative (VmIt) | Up to \~1,400,000 CU per leg; at most 160 iterations per session | Each leg is its own Solana transaction; adaptive opcodes/step — packed to each tx's CU budget |

Each Solana transaction has a default budget of 200,000 CU, extendable to \~1.4M CU via compute budget instructions (added automatically by the Rome SDK).

Iterative execution is not unlimited. One session ends after 160 iterations, and the Rust Rome SDK's iterative composer plans at most 128 legs. A plan the wallet signs itself goes through `rome_submitSignedV1Plan`, which accepts at most 64 transactions per call. Most calls need far fewer. Legacy-track CPIs are refused inside an iterative session, so a call that makes them has to fit the atomic budget. See [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls) and [Constraints](/core-concepts/constraints).

## CU Cost Estimates

### EVM Operations

| Operation                                  | Approximate CU       | Notes                                                                              |
| ------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------- |
| Transaction signature check (EVM-key lane) | \~25,000 CU          | The `secp256k1_recover` syscall, the same cost as the `ecrecover` precompile below |
| Simple transfer                            | \~50,000-100,000 CU  | Balance updates only                                                               |
| ERC-20 transfer                            | \~100,000-150,000 CU | Includes SPL precompile call                                                       |
| Contract deployment (small)                | \~200,000-400,000 CU | Depends on bytecode size                                                           |
| Storage write (SSTORE)                     | \~5,000-20,000 CU    | Cold vs warm access                                                                |

### Precompile Operations

| Precompile      | Approximate CU                        | Notes                                                                                                                                                                                                                                          |
| --------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ecrecover       | \~25,700 CU                           |                                                                                                                                                                                                                                                |
| SHA-256         | \~257 CU per 32-byte word measured    | Plus a small fixed cost. The program prices SHA-256, RIPEMD-160 and the identity precompile with one shared per-word figure of about 2,200 CU, sized to RIPEMD-160, the costliest of the three, so a SHA-256 call is charged more than it uses |
| BN254 ecAdd     | Close to 0 CU                         | Cheaper than copying 32 bytes                                                                                                                                                                                                                  |
| BN254 ecMul     | \~4,200 CU                            |                                                                                                                                                                                                                                                |
| BN254 ecPairing | \~22,000 CU plus \~12,800 CU per pair | A four-pair check is about 73,000 CU, well inside one atomic transaction                                                                                                                                                                       |

These figures come from the program's own calibration, which used compute units read back from a Solana node on a testnet deployment. They are not re-measured for this page; measure your own call with `computeUnitsConsumed`.

## Optimization Techniques

### 1. Use Yul for Hot Paths

Solidity's optimizer produces reasonable code, but Yul (inline assembly) can reduce CU significantly for critical operations:

```solidity
// Before: ~600K CU
function createPairAccount(bytes32 token0, bytes32 token1) external {
    // Solidity-level operations
}

// After: ~150K CU (Yul optimization)
function createPairAccount(bytes32 token0, bytes32 token1) external {
    assembly {
        // Direct memory manipulation, skip ABI encoding overhead
    }
}
```

### 2. Cache PDA Derivations

PDA derivation via `find_program_address` is expensive. Store derived PDAs in contract storage rather than computing them on every call:

```solidity
mapping(address => bytes32) private cachedPdas;

function getPda(address user) internal returns (bytes32) {
    bytes32 cached = cachedPdas[user];
    if (cached != bytes32(0)) return cached;

    bytes32 pda = RomeEVMAccount.pda(user);
    cachedPdas[user] = pda;
    return pda;
}
```

### 3. Hardcode Known Program IDs

Don't load program IDs from storage — use constants:

```solidity
// Expensive: reads from storage
bytes32 splTokenProgram = storage_program_id;

// Cheap: compile-time constant
bytes32 constant SPL_TOKEN_PROGRAM = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9;
```

### 4. Minimize Account Count

Each account in a Solana transaction adds CU overhead. Reduce the number of accounts by:

* Batching operations that share accounts
* Using fewer intermediate accounts
* Avoiding redundant ATA creation checks

### 5. Use Optimizer Settings

```typescript
// hardhat.config.ts — enable the optimizer in a build profile
solidity: {
  profiles: {
    default: { version: "0.8.28" },
    production: {
      version: "0.8.28",
      settings: { optimizer: { enabled: true, runs: 200 } },
    },
  },
}
```

Build with `npx hardhat compile --build-profile production`.

## Measuring CU Consumption

Use `eth_estimateGas` to measure CU before submitting:

```bash
cast estimate --rpc-url https://hadrian.testnet.romeprotocol.xyz/ \
  0xCONTRACT "myFunction(uint256)" 42
```

Or via ethers.js:

```javascript
const gas = await contract.myFunction.estimateGas(42);
console.log("Estimated gas:", gas.toString());
```

## What's Next

* [Constraints](/core-concepts/constraints) — full list of limits and boundaries


# Constraints

Limits when building on Rome EVM — Solana transaction size, \~1.4M CU budget, account caps, storage layout, and how Rome works around each.

Important limits and boundaries when building on Rome EVM. Understanding these constraints helps you design contracts that work reliably.

## Solana Transaction Limits

| Constraint           | Value              | Impact                                                                                                  |
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------- |
| Transaction size     | 4 KB (4,096 bytes) | Rome sends Solana v1 transactions; larger EVM txs are staged in holder accounts (transparent)           |
| Compute units per tx | \~1.4M CU          | Operations exceeding this use iterative mode, which has its own caps (below)                            |
| Accounts per tx      | 64 addresses       | The v1 transaction cap; everything the EVM tx and its CPIs touch, including Rome's own accounts, counts |
| Max holder size      | 80 KB              | Max RLP size for a single EVM transaction                                                               |

## EVM Execution Limits

| Constraint                          | Value                                                    | Notes                                                                                                                                                           |
| ----------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contract storage slots              | 256 per storage account                                  | Multiple storage accounts can be created per contract                                                                                                           |
| Opcodes per iteration               | Adaptive                                                 | Packed to each Solana tx's CU budget (VmIt)                                                                                                                     |
| Account lock TTL                    | 3 s (4 s when the session touches more than 28 accounts) | During iterative execution; renewed each step                                                                                                                   |
| Iterative session length            | 160 steps                                                | Past the cap the next step releases the session's locks and ends it without committing anything                                                                 |
| Legs planned by the Rust Rome SDK   | 128                                                      | The iterative composer's ceiling; real calls settle in far fewer                                                                                                |
| Transactions per wallet-signed plan | 64                                                       | The most `rome_submitSignedV1Plan` accepts in one call; see [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls) |
| Treasury wallets                    | 64                                                       | Fee pool wallets                                                                                                                                                |
| Contract size limit                 | 24 KB                                                    | Same as Ethereum (EIP-170, 24,576 bytes)                                                                                                                        |

## CPI Constraints

| Constraint                   | Value                                                                  | Notes                                                                                                                    |
| ---------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| CPI depth                    | 4 levels max                                                           | Solana's CPI depth limit                                                                                                 |
| Accounts per CPI call        | Within the 64-address transaction cap                                  | Shared with every other account the transaction touches                                                                  |
| CPI + Transfer Hook depth    | Uses CPI levels                                                        | Transfer hooks from inside CPI may exceed depth                                                                          |
| One track per contract       | Legacy (`0xff…07/08/09`, `0x42…16`) **or** cached (`0xff…04/05/06/0b`) | Mixing mutating calls from both in one transaction is refused                                                            |
| Legacy CPI in iterative mode | Not allowed                                                            | A legacy-track CPI in an iterative transaction fails with `CpiProhibitedInIterativeTx`; cached-track effects are allowed |

**CPI depth is the most critical constraint.** Rome EVM consumes one CPI level when Solana calls the Rome program. If your Solidity contract then calls another Solana program via CPI, that's level 2. If that program calls another, that's level 3. You have at most 4 levels total.

```
Level 0: Solana Runtime → Rome EVM Program
Level 1: Rome EVM Program → Your CPI target (e.g., Jupiter)
Level 2: Jupiter → another program (e.g., Raydium)
Level 3: Raydium → SPL Token (maximum depth)
```

## Token-2022 Transfer Hook Constraints

| Constraint              | Impact                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------- |
| One hook per mint       | A Token-2022 mint has a single transfer-hook slot                                   |
| `transfer_checked` only | Hooks don't fire on plain `transfer`. Rome bridge operations use `transfer_checked` |
| Mint/burn not hooked    | Controlled via mint authority, not hooks                                            |

## Gas and Pricing Constraints

| Constraint           | Notes                                                      |
| -------------------- | ---------------------------------------------------------- |
| Gas pricing source   | Meteora DAMM pool, v1 or v2 (SPL gas token)                |
| Gas price multiplier | Configurable per proxy (`gas_price_mul`)                   |
| Minimum gas price    | Set by proxy configuration                                 |
| Gas estimation       | Performed off-chain via Mollusk emulator before submission |

## Network-Specific Constraints

| Environment       | Chain ID | rome-evm Program ID                           |
| ----------------- | -------- | --------------------------------------------- |
| Hadrian (testnet) | 200010   | `RPTWwELXAY4KC9ZPHhaxp7Sq1hHtU3HNEgLbSegCcWf` |
| Rubicon (mainnet) | 7531     | `RomePq9X3iAoTr813HR7uRarafFDUJty6GdiEceYpzX` |

## Precompile Constraints

| Precompile                 | Constraint                                                                                                         |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Modexp (0x05)              | **Disabled** — a call reverts with `big_mod_exp call is disabled` until Solana activates the `big_mod_exp` syscall |
| BN254 ecPairing (0x08)     | About 22K CU plus 12.8K per pair by the program's pricing; a four-pair check fits one atomic transaction           |
| CPI precompile (0xFF...08) | Accounts must be declared upfront in the Solana transaction                                                        |

## Transaction Validation

| Rule                           | Effect                                                                                                                                                                        |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signature malleability (EIP-2) | A signed transaction whose `s` is above the curve order's half is rejected                                                                                                    |
| RLP shape                      | A transaction whose RLP list carries trailing bytes past its last item is rejected                                                                                            |
| Unimplemented opcode           | Aborts the whole transaction (`IncompatibleVersionEVM(opcode)`); nothing is committed and a calling contract cannot swallow it                                                |
| Gas metering                   | Rome has no per-opcode gas schedule: the gas passed to `CALL`/`CREATE` is not enforced, and there is no 2300-gas stipend — do not rely on stipend-based reentrancy protection |

## Oracle Constraints

| Constraint            | Value                                                        |
| --------------------- | ------------------------------------------------------------ |
| Default max staleness | 60 seconds                                                   |
| Historical round data | Not supported — `getRoundData(roundId)` reverts              |
| Switchboard EMA       | Not supported — `latestEMAData()` reverts on SwitchboardV3   |
| Parser offsets        | Empirically validated — must re-validate before redeployment |

## Design Recommendations

1. **Keep CPI depth shallow.** Design contracts to minimize nesting. If you're calling Jupiter which calls Raydium which calls SPL Token, you're at 3 levels — dangerously close to the limit.
2. **Prefer atomic mode.** Design operations to fit within \~1.4M CU. Iterative mode adds latency (3-4 second locks), a cap of 160 iterations per session, and a wallet prompt per leg on the Solana lane when the wallet signs the legs itself. A call that makes legacy-track CPIs cannot run iteratively at all.
3. **Declare accounts upfront.** All Solana accounts touched by CPI must be known at transaction creation time (the proxy's emulation discovers them). Dynamic account discovery within a CPI call is not possible, and the total must fit the 64-address cap.
4. **Pick one precompile track per contract.** Calling a custom Solana program (`invoke_signed`) is legacy-track and atomic-only; see [Integrate a Solana program](/developer-guides/integrate-solana-program).
5. **Use `transfer_checked`.** If you're building anything that touches Token-2022 tokens, always use `transfer_checked` to ensure hooks fire.
6. **Test CU consumption.** Measure real Solana compute units from the transaction receipt (`computeUnitsConsumed`) — EVM `gasUsed` is not a faithful proxy for Solana CU. Optimize with Yul for hot paths.

## What's Next

* [Compute Budget](/core-concepts/compute-budget) — detailed CU costs per operation
* [Token Interop](/core-concepts/token-interop) — ERC-20 ↔ SPL bridging model
* [Precompile reference](/reference/precompiles) — every selector and its dispatch class


# Token Interop

How ERC-20 and SPL tokens are the same underlying account on Rome.

Rome represents an ERC-20 token and its underlying SPL token as one shared account. This page explains how tokens work across EVM and Solana.

## The shared-state model

For an ordinary wallet, an ERC-20 token on Rome is a **transparent wrapper** over an SPL token account on Solana. The wallet's ERC-20 balance is its SPL balance. A program-derived address for the EVM address owns the token account. This is its **external-authority PDA**.

<figure><img src="https://2654533989-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-552c639322c2d0e90b312def5db39d06c1871d18%2Ftoken-interop.svg?alt=media" alt="An EVM address and its Solana token account, owned by its external-authority PDA. Multiple wrappers read the same token account. The wrapper escrow account and Solana pool vaults hold tokens of the same Solana mint."><figcaption></figcaption></figure>

* No bridging delay: the wallet's ERC-20 balance is its SPL balance.
* Ordinary wallets, including MetaMask wallets and synthetic addresses, have no liquidity fragmentation. Every wrapper over a mint shows the same wallet balance. The `balanceOf` method reads that balance from the wallet's token account. Tokens held inside EVM contracts are held per wrapper (see [The wrapper contract](#the-wrapper-contract)).
* For a wallet's tokens, nothing is locked on one side and minted on the other. Contract-held tokens sit in the wrapper's own escrow token account on Solana. Each wrapper tracks those tokens separately.

**Why a second wrapper does not split the balance**

Two wrappers over the same mint show the same wallet balance. Wrappers read `totalSupply` from the Solana mint. On Hadrian (testnet), three wUSDC wrappers returned matching wallet balances. All three wUSDC wrappers (9.0.0, 8.0.0 and May 2026) returned the Solana mint's supply.

Each factory allows one wrapper per mint. A new factory version can deploy another wrapper for that mint. The registry names the canonical wrapper per chain.

A Solana wallet holding USDC directly and an EVM address hold tokens of the same mint. Solana pools use the same vaults and reserves for both.

The holder table in the [Via explorer](/apps-on-rome/via) is built from each wrapper's `Transfer` events. It can differ from the token-account balance because direct SPL transfers, `ActivateAta`, and transfers through another wrapper do not appear in that wrapper's events.

> Import paths below use `@rome-protocol/rome-solidity`. The package is not on npm; install it from the public [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo with `npm install github:rome-protocol/rome-solidity`, or `forge install rome-protocol/rome-solidity` with a remapping; the wrapper and factory contracts also need OpenZeppelin under Foundry (see [Deploy Solidity Contracts](/developer-guides/deploy-solidity#using-the-rome-solidity-sdk)). Precompile interfaces live in [`contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol).

## The wrapper contract

`SPL_ERC20` (and its cached-track variant `SPL_ERC20_cached`, which the factory deploys for every mint except one with an armed transfer hook; see [Token-2022 mints](#token-2022-mints)) provide a full ERC-20 interface over an SPL mint:

* `balanceOf()` — reads the user's ATA balance from Solana (for a contract address it reads the wrapper's own ledger, see below)
* `transfer()` / `transferFrom()` — move tokens on Solana
* `approve()` / `allowance()` — per-spender ERC-20 allowances kept in EVM storage (an SPL token account has no EVM-style allowances). They are separate from the one-time grant below: to move a user's tokens at all, the wrapper acts as that user's SPL delegate, and the user gives it that right once per wallet
* `totalSupply()` — reads the SPL mint supply

A contract cannot send the one-time grant, so a contract's tokens are held in the wrapper's own token account and tracked in the wrapper's ledger. `balanceOf(contract)` reads that ledger.

## The factory

`ERC20SPLFactory` deploys a wrapper for any SPL mint:

```solidity
import {ERC20SPLFactory} from "@rome-protocol/rome-solidity/contracts/erc20spl/erc20spl_factory.sol";

// Deploy a wrapper, loading name/symbol from the mint's Token-2022 metadata or Metaplex
address wrapper = factory.add_spl_token_with_metadata(splMint);

// Or specify name/symbol manually
address wrapper = factory.add_spl_token_no_metadata(splMint, "USD Coin", "USDC");
```

Live factory address, from the `live` entries of the registry (v0.2.0): Hadrian `0x37c13e4ff21fb914eb24014dcbd0f8dbeaa31078`. Earlier factories are still on-chain but are marked `deprecated` or `retired` in the [registry](https://github.com/rome-protocol/rome-registry/tree/main/chains); do not use them. The registry is the source of truth, so read the live address from it (see [Reading the contracts list](/reference/contract-addresses#reading-the-contracts-list)) instead of copying one into your code.

## Canonical mints

There is no on-chain token-registry contract. Canonical wrappers and gas/bridge tokens are curated in the off-chain [`rome-protocol/registry`](https://github.com/rome-protocol/rome-registry); permissionless wrappers created via `add_spl_token_no_metadata` are discovered from the on-chain `TokenCreated` event. The registry names the canonical wrapper for each mint on each chain. Wallet balances are shared across wrappers. Each wrapper separately tracks the balances held by EVM contracts.

## SPL operations from Solidity

For user-PDA-signed SPL primitives, use the **HelperProgram** precompile (`0xFF…09`) — ATA creation, SPL transfers, and gas↔lamports conversion:

```solidity
import {IHelperProgram} from "@rome-protocol/rome-solidity/contracts/interface.sol";

IHelperProgram helper = IHelperProgram(0xFF00000000000000000000000000000000000009);

helper.create_ata(user, mint);              // create the user's ATA for a mint
helper.transfer_spl(to, tokens, mint);      // transfer SPL from the caller's PDA
```

`transfer_spl` has several overloads (including a delegate variant for `transferFrom` flows); see `interface.sol` for exact signatures. On the cached track, the equivalent operations live on `ISplCached` (`0xFF…05`) and `IAssociatedSplCached` (`0xFF…06`). A contract uses one track consistently.

## One-time grant per wallet

The wrapper moves a user's tokens as the **SPL delegate** of that user's token account. A wallet therefore has to grant it that right once per wrapper, before the wrapper can move anything for it. The grant is one call to the Helper precompile (`0xff00000000000000000000000000000000000009`):

```solidity
helper.approve_spl(wrapper, type(uint64).max, mint);   // approve_spl(wrapper, 2^64-1, mint)
```

`approve_spl` is signed by the user's own account, so the user sends it, not the wrapper or your contract. The mint is `mint_id()` on the wrapper.

* **Who needs it.** Every EVM wallet, and every Solana user's synthetic address, for each wrapper whose tokens it wants to move. A contract that pulls tokens with `transferFrom` works only for users who have granted the wrapper.
* **How to check.** Call `isEnabled(user)` on the wrapper. It returns `true` once the grant is in place.
* **What it looks like when it is missing.** The transfer reverts with SPL error `0x4` (owner mismatch).
* **One delegate per token account.** An SPL token account has a single delegate. Giving another program a grant on the same token account, such as a bridge or a router, replaces the wrapper's grant, and the wrapper stops working for that user until they grant it again. Check `isEnabled` before a transfer instead of assuming an earlier grant is still there.

When a new factory version replaces a wrapper, each wallet grants the new wrapper permission with `approve_spl`. The old wrapper can move that wallet's tokens only while it remains the token account's delegate. Granting the new wrapper replaces that delegate.

The [dual-lane app guide](/developer-guides/dual-lane-app#one-time-grant-the-wrapper) has the full code for both lanes.

## Deposit and withdraw

* **Into EVM** — the SPL side credits the user's PDA-owned ATA; the ERC-20 wrapper immediately reflects the balance. Cross-chain inbound transfers settle trustlessly via a user-signed authorization on the bridge.
* **Wrap gas into the gas token** — call the `Withdraw` precompile (`0x42…16`). `withdraw_to_ata(wei)` burns native gas and credits the equivalent amount, in the gas token's own decimals, to the caller's token account, which the gas token's wrapper (for example wUSDC) then shows as a balance. `withdraw_to_pda(wei)` pays lamports to the caller's PDA and applies only to a chain whose gas is SOL; it reverts on Hadrian and Rubicon, which use USDC as gas, so use `withdraw_to_ata` there. The amount is in 18-decimal gas units and must be a whole number of gas-token units: on a USDC-gas chain a multiple of 10^12 (one 6-decimal unit), otherwise the call reverts. `HelperProgram.deposit_from_ata(wei)` goes the other way and turns the gas token in the caller's token account back into native gas, with the same multiple-of rule. These are gas-token conversions, not an exit for other tokens.
* **Out to Solana, any other token** — the wrapper's own balance is already the SPL balance, so moving it out is an SPL transfer:
  * `HelperProgram.transfer_spl_to_signer(amount, mint)` sends the caller's tokens to the token account of the Solana wallet that signed the transaction. This return leg reaches the user's own wallet only on the wallet-signed Solana lane. Called from MetaMask or through the relay, the signer is Rome's proxy payer, so the call fails or the tokens go to Rome. Use `transfer_spl` or `RomeBridgeWithdraw.bridgeOutToSolana` there instead.
  * `RomeBridgeWithdraw.bridgeOutToSolana(solanaRecipient, amount, mint)` sends them to any Solana wallet. It needs the user's grant for the bridge contract (`approve_spl(bridge, …)`, see the one-delegate note above), and the recipient's token account must exist; `ensureRecipientAta(solanaRecipient, mint)` creates it.
  * `RomeBridgeWithdraw` also burns USDC to another chain over CCTP (`burnUSDC`) and sends other assets over Wormhole (`burnETH`, `burnToWormhole`).

Take the live `RomeBridgeWithdraw` address from the registry.

## Gas token

Each chain has its own gas token: SOL or an SPL token. The Rome Proxy's price source can be a Hermes API feed, a Meteora DAMM v1 pool, a Meteora DAMM v2 pool, or a fixed price. Hadrian (testnet) and Rubicon (mainnet) use USDC. Hadrian uses test USDC. Rubicon uses real USDC. There is no universal default gas token.

## Token-2022 mints

The factory also wraps Token-2022 mints. It asks the Helper precompile what the mint is, with `mint_info(mint)` (selector `0xe24bf5d4`), and picks the wrapper from the answer. `mint_info` returns five values:

| Value          | Meaning                                                                                       |
| -------------- | --------------------------------------------------------------------------------------------- |
| `tokenProgram` | the token program that owns the mint (the classic SPL Token program or Token-2022)            |
| `decimals`     | the mint's decimals                                                                           |
| `hookProgram`  | the transfer-hook program, or zero when there is no hook or the hook is present but not armed |
| `feeBps`       | the transfer fee in basis points, or zero when there is none or it is set to zero             |
| `extensions`   | a bitmap of the Token-2022 extensions the mint carries, bit N for extension type N            |

What a mint gets:

| Mint                                                                                         | Wrapper                                                                                                      | `wrapper_kind_by_mint` |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------- |
| An ordinary mint, a Token-2022 mint with no hook, or one whose hook is present but not armed | the cached wrapper (`SPL_ERC20_cached`)                                                                      | `1`                    |
| A Token-2022 mint with an armed transfer hook                                                | the hooked wrapper (`SPL_ERC20_Token2022Hooked`), deployed through the factory's `hooked_wrapper_deployer()` | `2`                    |

A mint with a transfer fee gets the cached wrapper, and its `Transfer` event reports the amount that arrived after the fee, not the amount sent. You can read the kind a mint got from the factory with `wrapper_kind_by_mint(mint)`; on the live Hadrian factory the USDC, ETH and SOL wrappers all return `1`.

What is refused:

* A mint with an armed transfer hook cannot get one of the ordinary wrappers. Deploying `SPL_ERC20` or `SPL_ERC20_cached` on it directly reverts with `ArmedTransferHookUnsupported(mint, hookProgram)`. Through the factory this case goes to the hooked wrapper instead.
* An address that is not an SPL mint makes `mint_info` revert with `Invalid SPL mint account: <address>`.
* A mint that already has a wrapper reverts with `Token exists`, and a symbol that is already in use reverts with `Token with symbol exists`.
* `add_spl_token_with_metadata` reads the name and symbol from the mint's own Token-2022 metadata first and from Metaplex second. If the mint has neither it reverts with `Mint asserts no name: use add_spl_token_no_metadata`.

How the hooked wrapper behaves:

* Token-2022 runs the hook on every transfer, and the hook needs accounts that depend on the transfer. The wrapper cannot work those out, so the caller resolves the full account list from the mint's extra-account-metas list and passes it in. The wrapper checks the fixed part of that list (the hook program and its validation account) and forwards it unchanged.
* The plain `transfer`, `transferFrom` and `bridgeOutToSolana` revert with `HookAccountPlanRequired`. Use `transferWithHookAccounts`, `transferFromWithHookAccounts` and `bridgeOutToSolanaWithHookAccounts`, which take the account list as an extra argument.
* If the mint's hook program changes after the wrapper was deployed, transfers revert with `HookConfigurationChanged`.
* It moves tokens by direct cross-program call, the legacy track, and does not use the cached track. A contract uses one track consistently, and calls on the legacy track run only in atomic execution, so a call that uses the hooked wrapper has to fit one atomic transaction.
* It keeps no ledger for contract balances. Tokens sent to a contract land in the contract's own token account, and the contract can send them on only after it has made the `approve_spl` grant to the wrapper itself.
* It needs the same [one-time grant](#one-time-grant-per-wallet) as any other wrapper.

## Constraints

* SPL token amounts are `uint64` (max 18,446,744,073,709,551,615)
* ERC-20 allowances (`approve` / `allowance`) are EVM storage. The wrapper moves tokens as the user's SPL delegate, which the user grants once per wrapper (see [One-time grant per wallet](#one-time-grant-per-wallet))
* ERC-20 wrapper symbols must be unique per factory

## What's Next

* [Contract Addresses](/reference/contract-addresses) — precompiles and per-chain addresses
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — CPI and SPL operations from Solidity
* [How Solana and the EVM meet on Rome](/core-concepts/how-solana-and-evm-meet)
* [Integrate a Solana program](/developer-guides/integrate-solana-program)


# How Solana and the EVM meet on Rome

How Solana wallets call Solidity contracts, EVM addresses call Solana programs, and Rome handles identity, tokens, fees, and execution limits.

This page explains how calls cross between Solana and the EVM on Rome. It covers who signs, where tokens live, who pays, and which Solana rules affect Solidity contracts.

Unless stated otherwise, the numbers and transactions on this page are from Hadrian (testnet) as of 19 October 2026.

## What is a synthetic address?

A synthetic address is the EVM identity of a Solana public key. Rome computes it as the last 20 bytes of `keccak256` of the public key: `keccak256(pubkey)[12..32]`. The mapping is fixed. It is called synthetic because no one holds a secp256k1 key for it: MetaMask cannot sign for it, and only the Solana key can authorize calls from it.

At the top-level contract call, both `msg.sender` and `tx.origin` are the synthetic address. Rome stores the EVM nonce under that address. The contract cannot distinguish this identity from a MetaMask user through those fields.

## How does a Solana user call a Solidity contract?

The wallet authorizes an unsigned EIP-1559 call through one of two lanes. The v1 lane is the current path. The legacy lane is the older path: the wallet signs and pays for the Solana transaction carrying `DoTxUnsigned`, and the Rome EVM program derives the synthetic address from that Solana signer.

In the v1 lane, the wallet signs a readable message containing `Rome authorization`, a newline, and the call hash as `0x` followed by 64 lowercase hexadecimal characters. The transaction carries this signature in an Ed25519 verification instruction at index 0. The Rome EVM program derives the synthetic address from the key that authorized the call.

The Rome Proxy composes the v1 transaction. The wallet then signs it and sends it to Solana. On the relay route, the Rome Proxy submits the transaction and pays SOL, and the synthetic address pays gas from its native balance in the chain's gas token (USDC on Hadrian and Rubicon).

When the wallet pays directly, the v1 lane costs 15,000 lamports: a 5,000-lamport signature fee, 5,000 lamports for the Ed25519 verification signature, and a 5,000-lamport treasury toll. The older legacy `DoTxUnsigned` lane has no Ed25519 instruction and costs 10,000 lamports. Both routes also pay rent for any accounts created. Neither direct route charges EVM gas. A legacy-lane transaction on Hadrian cost the wallet 10,000 lamports and reported `gasUsed = 0` and `effectiveGasPrice = 0`.

The SDK's `submitRomeTxV1` follows these steps:

1. Read the synthetic address's nonce with `eth_getTransactionCount`, estimate gas, and build the unsigned call.
2. Ask the wallet to sign the readable `Rome authorization` message.
3. Request a transaction from `rome_composeUnsignedAtomicV1`.
4. Verify the returned payer, call bytes, Ed25519 authorization, permitted programs, and absence of a priority fee.
5. Ask the wallet to sign the v1 transaction, send it to Solana, and poll the Rome Proxy for the EVM receipt.
6. If the call requires iterative execution, relay it through `rome_relayUnsignedV1` using the same authorization signature.

Use `fundSyntheticGas` to fund the native balance needed for a relay or `msg.value`. It deposits the gas token from the wallet's token account and credits the synthetic address's native balance. Native balances use 18 decimals.

To use tokens in a contract, the SDK funds the synthetic address, calls the contract, and returns any remaining tokens. `buildFundLeg` uses `ActivateAta` to move tokens from the wallet into the synthetic address's token account. The Solidity call uses those tokens through a wrapper. `buildSweepLeg` uses `transfer_spl` to send the remaining tokens back to the wallet.

The wallet must support signing Solana v1 transactions. Solflare supports this on Solana devnet; Phantom does not yet. Every call checks and increments the EVM nonce under the synthetic address to prevent replay.

See [Call EVM from Solana](/developer-guides/call-evm-from-solana), [Build a dual-lane app](/developer-guides/dual-lane-app), and [Rome SDK](/products/rome-sdk).

## How does an EVM user, or a contract, call a Solana program?

Every EVM address has a Solana program-derived address that owns its token accounts and can sign calls to Solana programs. This is its external-authority PDA. The Rome EVM program derives it from the seed `EXTERNAL_AUTHORITY` followed by the EVM address. The seeds contain no chain ID, so chains hosted by the same Rome EVM program share this authority.

A user or contract calls the CPI precompile at `0xFF…08` with the target program ID, account list, and instruction bytes. CPI means cross-program invocation. The precompile invokes the Solana program within the same Solana transaction.

`invoke` signs for the caller's external-authority PDA when the account list marks it as a signer. `invoke_signed` also supports PDAs derived with salts. Each salted PDA must appear in the account list as a signer.

The methods `account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, and `pdas_batch_derive` are read-only helpers. They do not invoke a Solana program.

The precompile refuses calls targeting the Rome EVM program itself. It also refuses an account list containing the Rome EVM program ID. A CPI may not name the Solana fee payer, except inside a plain System `Transfer` instruction.

A RomeDexRouter 2.0.0 swap on Hadrian followed these steps:

1. The router called `HelperProgram.create_ata` to ensure the user's output token account existed.
2. It derived the user's input and output token accounts and marked its own external-authority PDA as the signer.
3. It called `CPI.invoke` with the Solana swap program, the account list, and the swap instruction bytes.
4. SPL Token moved tokens from the user's account into the pool and returned output tokens to the user's other account. The router's PDA signed as the user's SPL delegate. The router held no tokens.
5. The router checked the received amount with `CPI.account_u64_at(dstAta, 64)`.
6. The receipt reported 10,001 gas: `5,000 + 5,000 + 1 = 10,001` lamports of Solana cost. Those costs were the signature fee, treasury toll, and priority fee. The Rome Proxy paid SOL, and the user paid gas in USDC.

See [Call Solana from EVM](/developer-guides/call-solana-from-evm), [Integrate a Solana program](/developer-guides/integrate-solana-program), and the [Precompile reference](/reference/precompiles).

## Who signs when a contract calls a Solana program?

Rome signs with the external-authority PDA of the immediate caller of the precompile. A contract therefore signs as itself. Only a call sent directly to the precompile by the user's own EVM or synthetic address signs with the user's PDA.

To let a router or adapter move a user's tokens, the user calls `approve_spl(spender, amount, mint)`. This sets the contract's external-authority PDA as the SPL delegate of the user's token account. It is Rome's version of ERC-20 `approve`.

Under `DELEGATECALL`, the precompile refuses the bare external-authority PDA as a signer. Salted authorities include the running contract in their derivation under `DELEGATECALL`.

## Every address has a PDA, so why don't the wrappers split liquidity?

For a wallet, a wrapper is a view of its SPL tokens. `balanceOf` reads the wallet's real token account on Solana. `totalSupply` reads the mint's supply.

The three wUSDC wrappers on Hadrian (testnet), 9.0.0 (`0xbce339ba…`), 8.0.0 (`0xae287c43…`), and May 2026 (`0x9fd4d58d…`), show the same balance for each wallet. All three return the mint's supply from `totalSupply`.

A Solana-native holder and an EVM holder use accounts of the same mint. The Solana wallet owns its token account directly. The EVM address's external-authority PDA owns its token account. `ActivateAta` and `transfer_spl` move tokens between these accounts. Both holders trade against the same Solana pool accounts and reserves.

Contract-held balances need different treatment. Tokens sent to a contract sit in the wrapper's escrow account. The wrapper tracks the contract's balance. A UniswapV2-style pair on Hadrian had a balance of 9,600,566 through wrapper 9.0.0 and 0 through wrapper 8.0.0. Use the canonical wrapper from the registry for pools, vaults, and lending markets.

Each token account has one SPL delegate slot. Granting permission to a router or bridge replaces the wrapper's grant. After a wrapper rotation, the user must grant the new wrapper permission with `approve_spl`. Grant the wrapper again if another delegate has replaced it.

Each factory permits one wrapper per mint. Wrappers from different factories can still read the same wallet token accounts. See [Token Interop](/core-concepts/token-interop).

## How does Rome get around the different semantics of Solana and the EVM?

| Difference                                   | What Rome does                                                                                                                                                                                               |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Accounts declared up front vs dynamic access | Emulates the call first and uses the touched accounts as the Solana instruction's account list.                                                                                                              |
| One transaction's compute and size limits    | Uses atomic (VmAt) execution when the call fits. Holder accounts carry larger payloads, and iterative (VmIt) execution splits work across transactions.                                                      |
| Account locks across legs                    | Renews locks on balance and storage accounts every leg. Locks last 3 seconds, or 4 seconds when the transaction names more than 28 accounts. An atomic write to a locked account fails with `AccountLocked`. |
| Gas token and fees                           | Counts Solana costs in lamports and converts them to gas-token charges with the transaction's gas price. Wallet-paid calls pay SOL directly.                                                                 |
| Blocks and slots                             | Uses the Solana slot for `block.number` and the Solana clock for `block.timestamp`.                                                                                                                          |
| Finality                                     | Confirms at Solana `confirmed`. Hercules re-checks indexed blocks at `finalized`.                                                                                                                            |
| Signatures                                   | Verifies EVM signatures with `secp256k1_recover`. Solana authorization uses Ed25519. Both produce a 20-byte EVM sender.                                                                                      |
| Rent                                         | Funds Solana account creation and growth through the fee payer. Charges the EVM sender through gas when a fee recipient is present.                                                                          |
| Decimals and integer width                   | Uses 18 decimals for native gas balances. Conversion to SPL units must be exact and fit `u64`. Wrappers use the mint's decimals and reject amounts above `uint64` maximum.                                   |
| Reentrancy and gas metering                  | Has no EVM opcode gas meter. The 2,300-gas stipend does not restrict calls, so contracts need explicit reentrancy guards.                                                                                    |
| CPI depth                                    | Runs the Rome EVM program at depth 1 and a CPI target at depth 2. Solana permits nesting through depth 5.                                                                                                    |
| Atomicity of Solana side effects             | Executes legacy CPIs immediately and aborts the whole transaction if their frame later reverts. Cached calls are recorded during execution and sent at commit. They are discarded when their frame reverts.  |
| `SELFDESTRUCT` and unknown opcodes           | Disables `SELFDESTRUCT`. It and other unimplemented opcodes abort the whole transaction with `IncompatibleVersionEVM`.                                                                                       |
| Nonces                                       | Stores and checks an EVM nonce per sender in its balance PDA. Gas estimation skips the nonce check.                                                                                                          |

### Accounts

The emulator runs the call against Solana accounts before submission. It records every account touched and whether it must be writable. That set becomes the Solana instruction's account list. A required account missing from the list fails the on-chain call.

A time-dependent branch can change between emulation and execution. If that change causes the call to exceed its compute budget, the Rome Proxy recomposes once at the next block.

### Transaction size and compute

Atomic (VmAt) execution fits within one Solana transaction, with about 1.4 million compute units and at most 64 accounts. The call must also fit the heap and instruction-trace limits.

A Solana v1 transaction holds at most 4,096 bytes. For larger EVM transaction payloads, the Rome Proxy writes 3,600-byte chunks into a `TxHolder` holder account with a maximum size of 80,000 bytes. Each chunk's signature fee is charged as gas.

Iterative (VmIt) execution saves the VM state in a `StateHolder` holder account between legs. A session permits at most 160 iterations. The planner produces at most 128 legs, with an opcode limit of at least 500 per leg. Legacy CPIs require atomic execution. Cached calls support iterative execution.

### Gas and rent

One Rome gas unit represents one lamport of Solana cost. Costs include signature fees, the 5,000-lamport treasury toll, priority fees, and rent net of refunds. `eth_gasPrice` reports gas-token wei per lamport. When a fee recipient is present, Rome charges the sender `lamports × tx.gasPrice` from the native balance.

The Solana fee payer funds rent when Rome creates or grows balance and storage accounts. That cost is billed to the EVM sender as gas when a fee recipient is present. A first transfer to a never-used address therefore costs about 1 million gas. `eth_estimateGas` never returns less than 21,000.

### Blocks, time and finality

`block.number` is the Solana slot. `block.timestamp` is the Solana clock's Unix timestamp. Rome captures both when it creates the VM state and keeps them across iterative legs.

`blockhash(n)` is `keccak256(bytes32(n))` within the last 256 slots and zero outside them. It has no entropy. `block.coinbase` is the fee recipient, or zero when there is none.

The SDK waits for `confirmed`. Hercules re-checks each indexed block at `finalized` and reloads it if the blockhash changed.

### Reverts, reentrancy and CPI

Legacy CPIs execute immediately. If a frame that made a CPI later reverts, Rome aborts the whole transaction. Solidity `try/catch` cannot absorb that revert. Cached calls are recorded during execution and sent at commit. A reverted frame discards its recorded calls. Once a transaction has written through one track, writes through the other track are refused. After a cached write, the legacy read helpers are refused too: CpiProgram's account reads and `pdas_batch_derive`, and HelperProgram's `user_balance`, `allowance_of` and `mint_info`.

Rome has no EVM opcode gas meter. `gasleft()` returns the gas limit, or `U256::MAX` when there is no fee recipient. The gas passed to `CALL` or `CREATE` is not enforced. Use explicit reentrancy guards instead of relying on the 2,300-gas stipend.

Solana's instruction stack depth is 5. The Rome EVM program runs at depth 1, so its target runs at depth 2 and can nest through depth 5. The Hadrian swap ran the swap program at depth 2 and SPL Token at depth 3. A pending Solana change (SIMD-0268) would raise the depth from 5 to 9. It was not active on devnet or mainnet as of 19 October 2026. A CPI back into the Rome EVM program is refused.

See [Execution Model](/core-concepts/execution-model), [Constraints](/core-concepts/constraints), [Compute Budget](/core-concepts/compute-budget), and [Known Limitations](/security/known-limitations).

## Common questions

### Are MetaMask and Phantom the same account on Rome?

No. A MetaMask key and a Solana key give two different EVM addresses, and Rome never links them. The MetaMask key gives its usual Ethereum address. The Solana key gives its [synthetic address](#what-is-a-synthetic-address), `keccak256(pubkey)[12..32]`: `2PMoUv…` becomes `0x45b64dae…`. At the top-level call, `msg.sender` and `tx.origin` are the signing address, including the synthetic address on the relay route.

Different keys cause the separation: two MetaMask keys are also two accounts. The curves only mean neither key can sign for the other's address. There is no linking feature. `ActivateAta` funds only the signer's own synthetic address. A `Deposit` instruction can credit any EVM address with native gas, paid from the signer's gas-token account. The proxy only builds deposits to the signer's own synthetic address. None of these makes two keys one account. A pool sees two holders, but both trade against the same reserves; see [Is liquidity shared](#is-liquidity-shared-between-solana-and-evm-users-or-split).

### How do Solana and EVM accounts map?

Rome maps each Solana key to one fixed [synthetic EVM address](#what-is-a-synthetic-address). Every EVM address has a balance PDA per chain for its nonce, native gas balance, and code. Its external-authority PDA owns its token accounts and signs CPIs. Chains on one Rome program share that authority.

These accounts were read on Hadrian (testnet):

| Account                      | MetaMask address `0x35153297…a266`                                        | Solana wallet `2PMoUv…`, synthetic address `0x45b64dae…127d`                                 |
| ---------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Wallet's own Solana accounts | None from the MetaMask key                                                | SOL system account `2PMoUv…`; own wSOL account `T8gKAAg9pz5eLcerH1Nh52Hn97m1nnGyhY9z4bwtyir` |
| Balance PDA                  | `8zuWEakFcuRRJ5Gju44u6Ptyk22gbNUMbsD8VLEQVS4c`                            | `EeRr14Feh3Ft7YuyacAVJQd3YSYsKAPTqdMEN43gtBSd`                                               |
| External-authority PDA       | `EZSTURZAg3GMqiWN3NBCvMLjEeLSxGH9PGooW6dAG8Kb`                            | `FMSHMpgdA9rEhzGLSvFniAHT7BmGjJweQqxPiGJJntC1`                                               |
| Token account Rome uses      | USDC `8wAzpmag4oL4c6ku6wqQBjxWXYLPoVsek6SyAvrYaxim`, owned by `EZSTURZA…` | wSOL `3pPjxR4ho3mVb5AMauVCjqTBUVS2Rif5JAnk9DFukPg2`, owned by `FMSHMpgd…`                    |

A Solana user's tokens sit in two places: the wallet's own accounts and the synthetic address's PDA-owned accounts that wrappers read. The SDK moves tokens in with `ActivateAta` and back with `transfer_spl`, as described in [How does a Solana user call a Solidity contract?](#how-does-a-solana-user-call-a-solidity-contract). The sweep `0x95b2502d…` returned 0.016587 wSOL to the wallet. The same MetaMask address has different token accounts on Hadrian (testnet) and Rubicon (mainnet) because they run on different Rome programs, mints and Solana clusters.

### Who pays which fee when an EVM user and a Solana user call the same contract? Is it one fee path or two?

Rome uses one cost model with two ways to settle it. A Rome gas unit is one lamport of Solana cost: signature fees, the 5,000-lamport treasury toll, any priority fee, and rent net of refunds.

On wallet-signed Solana lanes, the Solana fee payer pays that cost in SOL with no EVM gas charge. On MetaMask and relay calls, the Rome Proxy fronts SOL. The program charges the EVM sender `lamports × the transaction's gas price` from its native balance to a proxy fee recipient. The gas token is USDC on Hadrian (testnet) and Rubicon (mainnet).

These two `supply` calls (USDC from MetaMask, wSOL from the Solana key) used AavePool 2.0.0 (`0x81e7e7e4…`) on Hadrian (testnet). Each created two accounts.

| Cost or recipient       | EVM key (MetaMask)                                                                  | Solana key (v1 wallet-signed)                                                                                        |
| ----------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| EVM tx                  | `0xc7e6ee8d…`, from `0x35153297…`                                                   | `0xb0712551…`, from `0x45b64dae…`                                                                                    |
| Who signed and paid SOL | Rome's proxy payer `GNX1fLhb…`                                                      | The user's wallet `2PMoUv…`                                                                                          |
| SOL spent               | 2,102,962 lamports = 5,000 signature + 2 priority + 5,000 toll + 2 × 1,046,480 rent | 2,107,960 lamports = 10,000 in signature fees (transaction + Ed25519 verification) + 5,000 toll + 2 × 1,046,480 rent |
| EVM gas charged         | Sender paid 2,102,962 gas × 13,004,742,450 wei = 0.0273485 USDC                     | `gasUsed` 0, `gasPrice` 0                                                                                            |
| Gas paid to             | Proxy fee recipient `0x179d6112…`                                                   | Nobody; no `gasRecipient`                                                                                            |
| Treasury toll recipient | `7WDWCxvk…`, treasury account #31                                                   | `3F9eKFpr…`, treasury account #6                                                                                     |

* **Legacy lane (older path):** `0x350b8e36…` cost the wallet 1,056,480 lamports: 5,000 signature fee + 5,000 toll + 1,046,480 rent, for one created account. EVM gas was 0.
* **Relay:** Aave `borrow` transaction `0x18d65e8c…` used 16 Solana legs. The synthetic address paid 2,177,992 gas = 0.0226594 USDC, using the same fee path as a MetaMask user. The gas charge covers `16 × 5,002 + 5,000 toll + 2 × 1,046,480 rent = 2,177,992`. Each relay leg also carries the Ed25519 verification signature. The proxy pays that 5,000 lamports per leg and does not bill it, so this call cost the proxy 16 × 10,002 lamports in fees.
* **Shared treasury:** Every route pays its 5,000-lamport toll into one of the chain's 64 treasury accounts. The two supply calls above paid their tolls into accounts #31 and #6 of the same set.
* **Declared price:** Rome charges the transaction's declared price, `maxFeePerGas` for EIP-1559. There is no base fee or refund of the difference. Live EVM-key transactions declared from 1× to 2× the `eth_gasPrice` quote of 10,403,793,960 wei per lamport, and each paid exactly what it declared. Gas is priced per lamport of Solana cost, not per EVM opcode.
* **Wallet-signed costs:** The SDK lanes set no priority fee, so users pay no compute charge. They pay signature fees, the toll, and rent in SOL. The v1 lane pays for two signatures: the transaction signature and Ed25519 verification.

See the [Solana call flow](#how-does-a-solana-user-call-a-solidity-contract) and [dual-lane guide](/developer-guides/dual-lane-app) for the wallet and relay routes.

### Is liquidity shared between Solana and EVM users, or split?

Shared. Both lanes trade against the same mint's accounts. On Aave on Hadrian (testnet), a Solana key and a MetaMask-lane EVM key both supplied and borrowed through wSOL escrow account `3yY2FHSG…` (MetaMask: supply `0xaf460f52…`, borrow `0x89dbf0b6…`; Solana key: supply `0xb0712551…`, borrow `0x18d65e8c…`). This is the wSOL wrapper's escrow, which holds the awSOL reserve.

On rome-dex, a Solana wallet deposited liquidity directly into the same vaults used by EVM router swap `0xa7f77530…`. The Solana deposit transaction `2xFSzza7…` contains no Rome program. Only balances held by contracts depend on the wrapper, so use the registry's canonical wrapper for pools and vaults (see the [PDA and wrapper section](#every-address-has-a-pda-so-why-dont-the-wrappers-split-liquidity)). Don't measure wSOL liquidity with `totalSupply`: it reads 0 because the native SOL mint reports a supply of 0.

### How do I move tokens and gas balance between a MetaMask address and a Solana wallet?

* **Solana wallet to MetaMask address, tokens:** Create the token account if it is missing (the Associated Token program's idempotent create, with the address's external-authority PDA as owner), then send a plain SPL transfer to it. `HelperProgram.ata(address, mint)` returns its address. Transaction `2tBhefMJ…` did both and moved 20 USDC.
* **Solana wallet to MetaMask address, gas:** Fund your own synthetic address with `fundSyntheticGas`. Then send a native value transfer from the synthetic address to the MetaMask address.
* **MetaMask address to Solana wallet, tokens:** Call `transfer_spl(bytes32,uint64,bytes32)` on the Helper precompile directly from the EVM address, with the recipient's existing token account. Alternatively, call `RomeBridgeWithdraw.bridgeOutToSolana`, which needs a grant and an existing recipient token account.
* **MetaMask address to Solana wallet, gas:** Call the Withdraw precompile's payable `withdrawal(bytes32 to)` with the amount as `msg.value`. The wallet receives it as USDC in its USDC token account, which Rome creates if needed. `msg.value` must be a multiple of 10^12 wei. The recipient must be an existing wallet account.

`transfer_spl_to_signer` reaches your own wallet only on the wallet-signed Solana lane. From MetaMask or through the relay, the signer is Rome's proxy payer, so the call fails or the tokens go to Rome. See the [Precompile reference](/reference/precompiles).

One identity can spend the other's tokens only through an explicit grant, `approve_spl` or an ERC-20 `approve`, as with any other address. Each token account has one delegate slot. See [Token Interop](/core-concepts/token-interop#one-time-grant-per-wallet).

### Is my address the same on every Rome chain?

The EVM address is the same on every chain because it comes from the key. Its nonce and gas balance are kept per chain. Its token accounts are shared by chains on the same Rome program and differ between programs and clusters. See the [account mapping](#how-do-solana-and-evm-accounts-map).

### Which wallet works on the Solana lane? Does Phantom work?

Not yet on the Solana lane. The Solana lane uses Solana v1 transactions, and the wallet has to sign them. On devnet, Solflare does; Phantom does not yet.

### Does the explorer show which lane a transaction used?

Partly. The [Via explorer](/apps-on-rome/via) shows whether an EVM key or a Solana key sent a call: `origination` is `ecdsa` or `solana_unsigned`, and Solana-key calls carry `solanaSigner`. It does not tell the legacy, v1 and relay routes apart; all three show `solana_unsigned`. A relay call is the one with non-zero `gasUsed` and a `gasRecipient`. A gas top-up made with `fundSyntheticGas` shows as `ecdsa` with no `solanaSigner`.

## Where to go next

* [Architecture](/getting-started/architecture)
* [Execution Model](/core-concepts/execution-model)
* [Token Interop](/core-concepts/token-interop)
* [Call Solana from EVM](/developer-guides/call-solana-from-evm)
* [Call EVM from Solana](/developer-guides/call-evm-from-solana)
* [Build a dual-lane app](/developer-guides/dual-lane-app)
* [Integrate a Solana program](/developer-guides/integrate-solana-program)
* [Precompile reference](/reference/precompiles)
* [JSON-RPC reference](/reference/json-rpc)


# Choose your core

A dual-lane app can hold its core logic on the Solidity side or the native Solana side. It's a judgment call — weigh what matters for your app.

Most Rome apps are dual-lane: one shared state, reached by both MetaMask (EVM) and Phantom (Solana) users. You can build one in two mirror-image shapes, differing in **which side holds the core logic.** There's no hard-and-fast rule for which to pick — it's a soft judgment from what matters for *your* app: the code and assets you already have, your team's expertise, your security posture, the tooling you prefer, performance, and how each audience will use it.

## (a) Solidity core; Solana users via a synthetic sender

Your logic is a Solidity contract. A Phantom user's Solana signature is executed as an EVM transaction from their Rome-derived identity (the `external_auth` PDA) — they never need an EVM key. Often a good fit when you already have a Solidity contract (especially a hardened or audited one), want standard EVM tooling (Hardhat / Foundry / viem), or the logic is straightforward.

*Read:* [aerarium](https://github.com/rome-protocol/aerarium) — a Compound v3 lending market (Solidity core) that EVM and Solana users share.

## (b) Native Solana program core; EVM users via a thin CPI router

Your logic is a native Solana program; a thin Solidity router lets EVM users reach it via CPI. Often a good fit when you already have a Solana program, or when running the core natively matters for what you're building.

*Read:* [rome-dex](https://github.com/rome-protocol/rome-dex) — a native AMM (Solana core) with an EVM router, so both lanes trade one pool.

## For example: performance

Performance is one thing that *might* tip the choice — consider it as an example. Native Solana execution can be considerably cheaper than the EVM interpreter for heavy logic: one swap we measured ran roughly 6× cheaper natively. If your app has a hot, compute-heavy path, that kind of difference might matter to you; if it doesn't, it probably won't drive the decision. Treat it as one input among many, not a rule.

## Whichever you choose

Keep the core **authority-agnostic** — act on whichever authority signs, so the caller can be a Solana pubkey *or* an EVM user's PDA — keep the account set lean and ALT-friendly so the EVM lane stays cheap, and **test both lanes.**

## What's next

* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — the CPI mechanics for shape (b), and any Solidity→Solana call.
* [Compute Budget](/core-concepts/compute-budget) — more on execution cost.


# Rome SDK

Typed Solidity interfaces for calling Solana programs from EVM contracts, plus the Rust SDK behind Rome's off-chain services.

The Rome SDK provides typed Solidity interfaces for interacting with Solana programs from EVM smart contracts. It's the developer toolkit for building cross-runtime applications on Rome.

## SDKs

Rome has SDKs for three audiences — app builders (TypeScript), contract developers (Solidity), and teams running their own services on Rome (Rust).

### TypeScript SDK (`@rome-protocol/sdk`)

**For dapp and frontend developers** — the SDK most app builders start with. [`@rome-protocol/sdk`](https://github.com/rome-protocol/rome-sdk-ts) wraps the Rome write path so a web app submits Rome transactions correctly: `submitRomeTx` (the correct write path plus gas/fee handling), PDA / ATA derivation, CPI `invoke` / `invoke_signed` encoders, precompile bindings, and a `/bridge` subpath. Repo-first install (npm publish pending):

```bash
npm install github:rome-protocol/rome-sdk-ts#v0.4.0
```

```typescript
import { submitRomeTx } from '@rome-protocol/sdk';
// Submit any Rome EVM write through the correct write path (handles gas/fee encoding).
```

The public [rome-dex](https://github.com/rome-protocol/rome-dex) app uses only a few of the SDK's helpers (PDA and token-account derivation, the CPI `invoke` encoder and the 1.3x gas pad), and [cardo](https://github.com/rome-protocol/cardo) does not depend on it. Neither calls `submitRomeTx` or `submitRomeTxV1`, so read them for how an app calls the CPI precompile, not for the SDK's write path.

**Both lanes, one SDK.** `submitRomeTx` is the EVM lane (MetaMask). For the **Solana lane** — a Solana wallet driving your EVM app — `submitRomeTxV1` mirrors it: the proxy composes one Solana v1 transaction for the call, the wallet signs twice with the same key (a readable authorization message, then the transaction), and Rome runs the call as an EVM transaction from the user's derived identity, with no EVM key. A call that does not fit one transaction is relayed by the proxy with the same authorization signature, and `fundSyntheticGas` tops up the native gas a relayed call spends. A relayed call that reverts resolves with `receipt.status === "0x0"` instead of throwing, so check it; the [relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls) section has the gas rule and the limits. Value moves in and out through `buildFundLeg` / `buildSweepLeg` as an ERC-20 wrapper (e.g. `wUSDC`), not native `msg.value` — the synthetic sender holds nothing at rest — and a first-time synthetic is provisioned once, explicitly, with `buildCreatePdaCall`. The earlier `submitRomeTxSolanaLane`, which builds a legacy Solana transaction in the client, is still in the SDK. See [Build a dual-lane app](/developer-guides/dual-lane-app) and [Call EVM from Solana](/developer-guides/call-evm-from-solana).

### Solidity SDK (`@rome-protocol/rome-solidity`)

**For Solidity developers.** Provides the precompile interfaces, ERC-20/SPL wrappers, PDA derivation, and CPI utilities. The npm publish is pending; today you consume these from the public [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo (git dependency or copied files).

```solidity
import {ISystemProgram, ICrossProgramInvocation, IHelperProgram, IWithdraw}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";
import {SPL_ERC20} from "@rome-protocol/rome-solidity/contracts/erc20spl/erc20spl.sol";
import {RomeEVMAccount} from "@rome-protocol/rome-solidity/contracts/rome_evm_account.sol";
```

### Rust SDK (`rome-sdk`)

**For teams that run their own services on Rome.** A Rust workspace that handles transaction composition, Solana interaction, gas pricing, and block indexing. The Proxy and Hercules are built on it. The source is in [`rome-protocol/rome-sdk`](https://github.com/rome-protocol/rome-sdk). It sends Solana v1 transactions (4 KB, up to 64 accounts).

**Building it.** The workspace expects the repositories [`rome-sdk`](https://github.com/rome-protocol/rome-sdk), [`rome-evm`](https://github.com/rome-protocol/rome-evm) and [`mollusk`](https://github.com/rome-protocol/mollusk) cloned side by side, because it depends on the other two by relative path. That is also why the supported way to use it from another project is a path dependency, not a plain git dependency. You need Rust 1.85 or later and network access for the first build. `rome-evm` has no default network feature, so any build that compiles it needs exactly one of `ci`, `testnet` or `mainnet`; use `ci` for local work, for example `cargo test -p rome-sdk --features ci`. The repository README has the full steps and the feature table.

**Licence.** The Rust SDK is source-available, not open source: it is all rights reserved and may be used for personal, non-commercial purposes. Commercial use needs written permission from Rome Protocol; the repository's `LICENSE` says how to ask.

## Solidity SDK: What's Included

### Precompile Interfaces

Bind an interface to its precompile address:

```solidity
ISystemProgram          constant System   = ISystemProgram(0xFF00000000000000000000000000000000000007);
ICrossProgramInvocation constant Cpi      = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
IHelperProgram          constant Helper   = IHelperProgram(0xFF00000000000000000000000000000000000009);
IWithdraw               constant Withdraw = IWithdraw(0x4200000000000000000000000000000000000016);
// Cached track: ISplCached 0xff…05, IAssociatedSplCached 0xff…06, ISystemCached 0xff…04, IWithdrawCached 0xff…0b
```

### SPL Token Operations

Use `IHelperProgram` (`0xff…09`) for user-PDA-signed SPL primitives:

```solidity
// Create the caller's ATA for a mint
Helper.create_ata(user, mint);

// Transfer SPL from the caller's PDA
Helper.transfer_spl(to, tokens, mint);
```

On the cached track, the equivalent operations live on `ISplCached` (`0xff…05`) and `IAssociatedSplCached` (`0xff…06`). See `interface.sol` for all overloads; a contract uses one track consistently.

### PDA Derivation

```solidity
// Derive a user's Solana PDA
bytes32 userPda = RomeEVMAccount.pda(msg.sender);

// Derive PDA with salt (for creating multiple PDAs per user)
bytes32 pda = RomeEVMAccount.pda_with_salt(msg.sender, salt);

// Find arbitrary PDA
(bytes32 pda, uint8 bump) = SystemProgram.find_program_address(programId, seeds);
```

### Cross-Program Invocation

```solidity
// Call any Solana program
ICrossProgramInvocation.AccountMeta[] memory accounts = new ICrossProgramInvocation.AccountMeta[](2);
accounts[0] = ICrossProgramInvocation.AccountMeta(signerPda, true, true);
accounts[1] = ICrossProgramInvocation.AccountMeta(targetAccount, false, true);

CpiProgram.invoke(programId, accounts, instructionData);

// Call with PDA signing
CpiProgram.invoke_signed(programId, accounts, data, seeds);

// Read account data
(uint64 lamports, bytes32 owner, bool isSigner, bool isWritable, bool executable, bytes memory data)
    = CpiProgram.account_info(pubkey);
```

### ERC-20 over SPL Tokens

```solidity
// Deploy wrapper for any SPL mint
ERC20SPLFactory factory = ERC20SPLFactory(FACTORY_ADDRESS);
address wrapper = factory.add_spl_token_with_metadata(splMint);

// Use the wrapper as standard ERC-20
SPL_ERC20 token = SPL_ERC20(wrapper);
token.transfer(recipient, amount);
uint256 balance = token.balanceOf(user);
```

### Borsh Deserialization

```solidity
import {Convert} from "@rome-protocol/rome-solidity/contracts/convert.sol";

// Parse Solana account data (little-endian Borsh format)
(uint64 value, uint256 newOffset) = Convert.read_u64le(data, offset);
(bytes32 pubkey, uint256 newOffset2) = Convert.read_bytes32(data, offset);
```

### Metaplex Metadata

```solidity
import {MplTokenMetadataLib} from "@rome-protocol/rome-solidity/contracts/mpl_token_metadata/lib.sol";

// Load token metadata from Metaplex
MplTokenMetadataLib.Metadata memory meta = MplTokenMetadataLib.load_metadata(
    mintPubkey, mplProgramId, cpiAddress
);
string memory name = meta.name;
string memory symbol = meta.symbol;
```

## Rust SDK: Architecture

The Rust SDK is a Cargo workspace. Its core crates:

| Crate               | Purpose                                                                                                                                            |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rome-sdk`          | Core API: `Rome` struct, config, transaction types (RheaTx, RemusTx, RomulusTx)                                                                    |
| `rome-evm-client`   | EVM rollup client, TxBuilder, ResourceFactory, emulator integration                                                                                |
| `rome-solana`       | Solana tower, RPC client, transaction batching and tracking                                                                                        |
| `rome-utils`        | RLP, hex, JSON-RPC, authentication utilities                                                                                                       |
| `rome-obs`          | OpenTelemetry observability (traces, metrics, logs)                                                                                                |
| `rome-meteora`      | Meteora DAMM v1 and v2 pool readers for gas pricing                                                                                                |
| `rome-meta-hook`    | Client for the Meta-Hook Router program: Token-2022 transfer-hook configuration, extra account resolution, transfers with compute-budget estimates |
| `rome-jito-bundler` | Jito bundle builder for atomic multi-transaction submission                                                                                        |

### Transaction Types

```rust
// Single rollup transaction
let rhea = RheaTx::new(signed_eth_tx);
let mut tx = rome.compose_rollup_tx(rhea).await?;
let sig = rome.send_and_confirm(&mut *tx).await?;

// Cross-rollup atomic transaction
let remus = RemusTx::new(vec![tx1, tx2]);
let mut tx = rome.compose_cross_rollup_tx(remus).await?;

// Cross-chain atomic transaction (EVM + Solana)
let romulus = RomulusTx::new(eth_txs, sol_ixs);
let mut tx = rome.compose_cross_chain_tx(romulus, signers).await?;
```

### Resource Pooling

The SDK pools Solana keypairs (payers) and holder account indices for parallel transaction submission:

```rust
let resource = resource_factory.get().await?;
let payer = resource.payer();       // Solana keypair
let holder = resource.holder();     // Holder account index
// Resource automatically returned to pool on Drop
```

## SDK Roadmap

### Built and Working

* SPL Token wrappers and precompile interfaces
* Meteora DAMM v1 swaps via CPI
* Oracle Gateway V1 + V2 (Pyth Pull, Switchboard V3)
* System Program helpers, Borsh deserialization
* ERC20SPL Factory + bridge contracts

## What's Next

* [Deploy Solidity](/developer-guides/deploy-solidity) — deploy your first contract using the SDK
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — use CPI to interact with Solana programs
* [Contract Addresses](/reference/contract-addresses) — deployed SDK contract addresses


# Rome CLI + MCP

The Rome developer CLI and MCP server — grounded chain facts, the right build pattern, and the Rome-unique actions (fund, deploy, verify) for a human or an AI agent.

`rome` carries a builder — human or AI agent — across the whole build-on-Rome lifecycle: grounded chain facts, the right pattern, contract calls, the funding on-ramp, deploy/send, cross-VM diagnosis, and a both-lane works-gate. One capability core, two aligned surfaces — a CLI (`rome <command>`) and an [MCP](https://modelcontextprotocol.io) server (`rome mcp`) — so an agent and a human share one mental model.

## Install

Repo-first (npm publish pending):

```bash
# one-shot, no install:
npx github:rome-protocol/rome-cli#v0.11.0 facts chain hadrian

# durable install — clone + link:
git clone --branch v0.11.0 https://github.com/rome-protocol/rome-cli
cd rome-cli && npm install && npm install -g .
```

A plain `npm install -g github:rome-protocol/rome-cli` does **not** work today — npm prepares the package's git dependencies without their own node\_modules ([rome-cli#25](https://github.com/rome-protocol/rome-cli/issues/25)). Use the npx one-shot or clone + link, and pin a tag as above: npx keeps the copy it fetched first, so an unpinned `npx github:rome-protocol/rome-cli` can keep running an old version, while a pinned tag always fetches that version.

## Two layers over one core

**Reads — grounding + diagnosis.** No keys; on both the CLI and the MCP server.

| Command                                                  | What it returns                                                                                               |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `rome facts chain·tokens·contracts·gas·balance·programs` | live chain facts from the registry + RPC — no hallucinated ids, addresses, or selectors                       |
| `rome cookbook patterns·cpi-recipe·errors`               | which example fits your goal · the CPI account-rules agents get wrong · decode a Rome failure → cause + fix   |
| `rome call <chain> <addr> <sig> [args]`                  | read a contract (`eth_call`) — multi-value args go comma-separated in one argument: `"0xOwner…,0xSpender…"`   |
| `rome doctor <chain>`                                    | preflight — chain live? RPC reachable? program configured? wallet funded?                                     |
| `rome tx <chain> <hash>`                                 | diagnose a tx — EVM receipt + the Solana settlement tx(s) + a Via link (Rome has no `debug_traceTransaction`) |
| `rome preset foundry\|hardhat <chain>`                   | a ready Rome network config for your toolchain + the quirks                                                   |

**Actions — sign on-chain.** CLI-only, key from the environment, **never** on MCP.

| Command                                                                    | What it does                                                                                                                                                                                                                                                                     |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rome deploy <chain> <artifact>`                                           | deploy a compiled contract, handling Rome's gas quirks                                                                                                                                                                                                                           |
| `rome send <chain> <addr> <sig>`                                           | write to a contract via the correct Rome write path                                                                                                                                                                                                                              |
| `rome fund <chain> --from <src> --amount <usdc>`                           | bridge USDC → Rome gas (CCTP) — the "from home" on-ramp                                                                                                                                                                                                                          |
| `rome bridge <chain> --from <src> --amount <usdc> [--intent gas\|wrapper]` | bridge USDC **in** as gas or wUSDC                                                                                                                                                                                                                                               |
| `rome bridge <chain> --to <dest> --amount <usdc>`                          | bridge wUSDC **out** — burn on Rome; you claim on the destination (Rome sponsors inbound, not the outbound claim)                                                                                                                                                                |
| `rome activate <chain>`                                                    | one-time PDA funding required before the **first bridge out** (inbound is frictionless — needs none)                                                                                                                                                                             |
| `rome verify <chain> [--path …]`                                           | the **path-aware works-gate**, one per way in: `solidity` — the same contract answers on the EVM lane *and* the Solana lane · `solana-program` — an EVM-lane call drives your Solana program via CPI · `from-home` — bridge in → act on Rome → bridge out, the round trip proven |

## Wire it into an AI agent (MCP)

You don't run or host anything — your MCP client launches `rome mcp` (a stdio server) on demand. Register it once:

```json
{ "mcpServers": { "rome": { "command": "npx", "args": ["-y", "github:rome-protocol/rome-cli#v0.11.0", "mcp"] } } }
```

(With the clone + link install, `{ "command": "rome", "args": ["mcp"] }` works too — the npx form just needs no prior install.)

The agent then calls tools like `facts_chain`, `cookbook_cpi_recipe`, `doctor`, `tx`, and `preset` — grounded on the live registry + RPC, so it stops guessing addresses and selectors. The MCP surface is **read-only and holds no keys** — safe to wire into any agent; it can never sign or move funds. The signing actions stay on the CLI, with the key supplied through the environment.

## The agent grounding loop

1. `rome cookbook patterns <what I'm building>` → which example repo + architecture
2. `rome facts chain <chain>` → RPC, program id, gas token (no hardcoding)
3. …write code against those exact values…
4. `rome verify <chain> --path <your way in>` → prove it works, whichever path you came by

## Learn more

* Repo + full guides: [`rome-protocol/rome-cli`](https://github.com/rome-protocol/rome-cli)
* [Ecosystem & repos](/getting-started/ecosystem) · [Build a dual-lane app](/developer-guides/dual-lane-app) · [From home: reach Rome from another chain](/developer-guides/from-home)


# App Sovereignty

Launch your own sovereign EVM chain on Solana — custom chain ID, any SPL token as gas, and gas revenue that accrues to your app's treasury.

App Sovereignty lets any Solana application launch its own EVM environment with a custom chain ID, custom gas token, and gas revenue that flows to the application — not a shared protocol.

## Overview

Each application on Rome gets a sovereign EVM instance:

* **Own chain ID** — your app is its own chain
* **Own gas token** — any SPL token (your project token, USDC, SOL)
* **Gas revenue** — all transaction fees accrue to your treasury
* **Full EVM tooling** — your users connect MetaMask, your devs use Hardhat/Foundry
* **Shared state** — EVM users and Solana users share the same application state and liquidity

## How It Works

1. **Onboard through the Sovereign Portal** — request a chain and configure your chain ID and gas token (any SPL mint) via the [Sovereign Portal](https://sovereign.devnet.romeprotocol.xyz)
2. **Rome brings up the chain** — the Rome team provisions and operates the chain's infrastructure
3. **Deploy contracts** — standard Solidity deployment with Hardhat or Foundry
4. **Users connect** — MetaMask with your chain ID, or any EVM wallet

Every live Rome chain — including the public reference chain [**Hadrian**](/networks/hadrian) — is recorded in the public [Rome registry](https://github.com/rome-protocol/rome-registry) (chain id, RPC, contract addresses), so a sovereign chain is discoverable through the same canonical source builders already read.

## Gas Token Pricing

Custom gas tokens are priced via a Meteora DAMM pool — v1 or v2, configurable (a Pyth/Hermes fallback is also available). The Proxy configuration specifies a `price_manager` that reads the pool to convert between your gas token and SOL for underlying Solana transaction fees.

```yaml
# Proxy config for custom gas token
price_manager:
  type: "meteora_damm_v1_pool"   # meteora_damm_v1_pool | meteora_damm_v2_pool | hermes_api
  pool_address: "YOUR_METEORA_POOL_ADDRESS"
```

## Use Case: Prediction Market Launches EVM Chain

```
Orra (Solana prediction market) wants EVM users:
  1. Rome deploys Orra's sovereign EVM with ORRA token as gas
  2. EVM users connect MetaMask, bridge USDC, buy prediction shares
  3. Under the hood: rUSDC (ERC-20) → unwrap to SPL → Orra CPI → buy shares
  4. Orra earns gas fees in ORRA token from every EVM transaction
  5. Single state: Solana users and EVM users see same markets, same liquidity
```

## Status

**Live** — active product with partners.

## What's Next

* [Architecture](/getting-started/architecture) — how Rome EVM works under the hood
* [Quickstart](/getting-started/quickstart) — deploy your first contract
* [Deploy Solidity](/developer-guides/deploy-solidity) — deployment guide for Hardhat and Foundry


# Oracle Gateway

Pyth and Switchboard price feeds behind Chainlink's AggregatorV3Interface.

The Oracle Gateway exposes Solana-native price feeds (Pyth, Switchboard V3) through Chainlink's `AggregatorV3Interface`, so Ethereum protocols porting to Rome keep their existing oracle integration code. **Oracle Gateway V2 is deployed on Hadrian and Rubicon**, though the registry lists feed addresses only for Hadrian, and a customer portal handles registration. A Rome-run keeper refreshes the cached and book feeds, but not every feed is fresh at every moment — [which feeds answer today](#which-feeds-answer-today) is spelled out below. The adapters, factory, and keeper model are covered in the [rome-oracle-gateway](https://github.com/rome-protocol/rome-oracle-gateway) repo.

> **Preview.** The Oracle Gateway has not been audited. The portal says the same: do not use it in production value flows.

## The problem

Ethereum DeFi expects Chainlink's `AggregatorV3Interface`:

```solidity
(, int256 price,,,) = priceFeed.latestRoundData();
```

Solana's oracle providers (Pyth, Switchboard) have different data formats. Without adaptation, every ported protocol would need custom oracle code.

## The solution

Oracle Gateway V2 deploys lightweight adapter contracts that:

1. Read price data directly from Pyth or Switchboard accounts on Solana (cross-state reads, not CPI)
2. Parse the on-chain data
3. Normalize prices to 8 decimals
4. Expose the standard Chainlink `AggregatorV3Interface`

```solidity
import {IAggregatorV3Interface} from "@rome-protocol/rome-solidity/contracts/oracle/IAggregatorV3Interface.sol";

// Same interface as Chainlink on Ethereum
(, int256 price,,,) = IAggregatorV3Interface(ADAPTER).latestRoundData();
// price = SOL/USD at 8 decimals (e.g. 15000000000 = $150.00)
```

## Consume a feed in three steps

1. **Resolve the adapter address from the registry** — never hardcode it:

```ts
import { getOracle } from "@rome-protocol/registry";
const solUsd = getOracle(200010).feeds["SOL/USD"].address;
// Hadrian, registry v0.2.0: 0x567384ee000ca0418c386A73eC2b7466f9f99D6b
```

The registry is installed from GitHub (`npm install github:rome-protocol/rome-registry#v0.2.0`). The feed keys carry a suffix that says which adapter kind sits behind them; see [Feed keys in the registry](#feed-keys-in-the-registry) before you pick one.

2. **Read it** with the interface your protocol already uses — `IAggregatorV3Interface(solUsd).latestRoundData()`. A stale or uninitialized feed **reverts** rather than serving a frozen price.
3. **Several feeds at once?** `BatchReader.getLatestPrices(adapters[])` (Hadrian, registry v0.2.0: `0x2869f0a96572e7fb15036656e9a32fd17346499b`) — and `getFeedHealth(adapters[])` before you depend on them. Cached adapters make each consumer read a plain `SLOAD`, so multi-feed transactions (a multi-collateral borrow, a portfolio view) stay cheap.

[cardo](https://github.com/rome-protocol/cardo) reads these feeds on Hadrian. Because the surface is exactly `AggregatorV3Interface`, Compound- and Aave-class protocols can take these feeds without oracle code changes — point their price-feed configuration at the adapter addresses.

## Adapter types

* **PythPullAdapter** — reads Pyth price accounts (price, confidence, EMA, publish time).
* **SwitchboardV3Adapter** — reads Switchboard aggregator accounts (price, timestamp; no EMA).
* **CachedPyth / CachedFeed adapters** — cached-track variants for CU-efficient reads.
* **BookFeedAdapter** — a read-only Chainlink-compatible view over one entry of a `PriceBook` contract. The book holds the prices for all of its registered feeds and is refreshed in batches (the refresh is permissionless); each feed's adapter keeps no cache of its own. A paused entry reverts straight away with `AdapterPaused`, instead of waiting to go stale.

Adapters are deployed as EIP-1167 minimal-proxy clones: the `OracleAdapterFactory` clones the Pyth, Switchboard and cached adapters, and each `PriceBook` clones its own `BookFeedAdapter` when a feed is registered. They expose an extended interface alongside `AggregatorV3Interface` (`latestPriceData`, `maxStaleness`, `oracleType`, and a `metadata()` surface used for health checks).

## Feed keys in the registry

`getOracle(chainId).feeds` has several keys per asset. The suffix on the key names the adapter kind:

| Key                  | Adapter kind                         | How it reads                                                                              |
| -------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- |
| `SOL/USD`            | Direct Pyth (`PythPullAdapter`)      | Parses the Pyth account on every call.                                                    |
| `SOL/USD-BOOK`       | `BookFeedAdapter`                    | Reads a `PriceBook` entry that the keeper refreshes.                                      |
| `SOL/USD-CACHED`     | Cached (wraps another feed)          | Reads a stored price that the keeper refreshes.                                           |
| `SOL/USD-CACHEDPYTH` | Cached Pyth (`CachedPythAdapter`)    | Reads a stored price that the keeper refreshes.                                           |
| `SOL/USD-SB`         | Switchboard (`SwitchboardV3Adapter`) | Parses the Switchboard account on every call. The registry marks these entries `retired`. |

Which one to consume depends on the chain. Check the next section first.

### Which feeds answer today

These are results of read-only `latestRoundData()` calls on the feeds listed in registry v0.2.0, made while this page was written. Feed freshness changes from minute to minute on devnet, so treat it as a snapshot and run the same call on the feed you plan to use.

* **Hadrian.** The direct feeds are often stale: in repeated checks most of them (BONK, BTC, ETH, JITOSOL, JTO, JUP, MSOL, USDC, USDT) reverted with `StalePriceFeed` every time, and `SOL/USD` answered on some calls and not others. The `-BOOK` feeds were the most reliable and answered on every call for every asset except JTO, whose `-BOOK` feed reverted as stale. Every `-CACHED` and `-CACHEDPYTH` feed reverted with `UninitializedPriceFeed` (nothing has refreshed them), and `SOL/USD-SB` reverted with `StalePriceFeed`. On Hadrian, prefer the `-BOOK` feed, and run `latestRoundData()` on the feed you choose before you depend on it.
* **Rubicon.** The registry does not list oracle feeds for Rubicon, so this page does not cover them. The factory is deployed there (see [Deployed addresses](#deployed-addresses)) and its first adapters answer, but look up the feed addresses from the factory (`adapterCount()` and `allAdapters(i)`) rather than from the registry.

A revert here is the designed behaviour: a stale or uninitialized feed fails loudly instead of returning an old price. Handle it in your contract.

## Freshness — the oracle-keeper

Adapters enforce a `maxStaleness` window: if `block.timestamp - publishTime > maxStaleness`, reads revert. On Solana mainnet, Pyth runs production keepers. On Solana devnet (where Hadrian lives), Pyth's pushers are best-effort, so Rome runs its own **oracle-keeper** sidecar to keep the underlying Pyth accounts fresh and to refresh the cached and book feeds. The keeper does not cover every feed on every chain at every moment (see [which feeds answer today](#which-feeds-answer-today)).

Check feed health with `BatchReader.getFeedHealth` (which reads each adapter's `metadata()`), not just `latestRoundData` — a feed can be reachable but stale.

## Customer portal

Register feeds and configure consumers per chain through the [Oracle Gateway portal](https://oracle.testnet.romeprotocol.xyz/). See the [portal page](/apps-on-rome/oracle-gateway) for what it covers.

## Deployed addresses

Canonical per-chain addresses (factories, adapter implementations, and the live feeds) live in the [registry](https://github.com/rome-protocol/rome-registry/tree/main/chains). Headline factories, from the `live` entries of registry v0.2.0 (version 5.0.0 on Hadrian; the Rubicon addresses are the ones on the [Contract Addresses](/reference/contract-addresses) page); see [Reading the contracts list](/reference/contract-addresses#reading-the-contracts-list):

| Contract             | Hadrian                                      | Rubicon                                      |
| -------------------- | -------------------------------------------- | -------------------------------------------- |
| OracleAdapterFactory | `0x52b008771fc95b4674a4f3533570b3d223f27f68` | `0x6a1340dd86c3951b6f13d9001b3951d56a2fe581` |
| BatchReader          | `0x2869f0a96572e7fb15036656e9a32fd17346499b` | `0x580dc0ecb226530ee8a90b57e0f0cd2555fb4f0f` |

## Constraints

* **No historical round data** — `getRoundData(roundId)` reverts; only `latestRoundData()` is supported.
* **Switchboard EMA not supported** — EMA data is Pyth-only.
* **Per-chain staleness** — `maxStaleness` is set per environment at bring-up: direct (Pyth-read) adapters default to \~60 seconds; cached adapters default to 3600 seconds (when the keeper is refreshing them, it does so far more often than that).
* **Parser offsets** are validated against current Pyth/Switchboard layouts; layout changes require re-validation.

## What's Next

* [Oracle Gateway portal](/apps-on-rome/oracle-gateway) — register feeds, configure consumers
* [Contract Addresses](/reference/contract-addresses) — precompiles and per-chain addresses


# Overview

Run an EVM rollup whose transaction data and proved state roots settle on Solana.

Rome ZK EVM lets you run your own EVM rollup on Solana. Your sequencer executes transactions, and your batcher posts them to Solana in batches. A prover runs the chain's batch guest with ZisK. Solana checks the proof and records the new state root. A proved root is final in the transaction that posts it.

Solana holds the transaction data, verifies the proofs and settles the roots. The chain does not depend on a separate data availability committee or another settlement chain. You run the node, batcher and prover for your chain. Rome's shared programs handle settlement on Solana devnet.

This section covers **v0.3.0**. The code and the full operator documentation are public in [solana-zk-evm](https://github.com/rome-protocol/solana-zk-evm/tree/v0.3.0). The batch guest is in [rome-zk-guest](https://github.com/rome-protocol/rome-zk-guest/tree/v0.3.0). Start with [Run a rollup](/rome-zk-evm/run-a-rollup) if you want to bring up a chain.

## What works on devnet

Anyone can register a chain on Solana devnet. Registration locks a 5 SOL deposit and creates the chain's settlement accounts. To post a proved root, the chain needs a guest built for its genesis and a verification key registered by Rome. Each proved root costs a 0.001 SOL settlement fee, plus Solana fees and account rent.

The deposit becomes refundable to the chain authority after the chain's first final root or ten posted roots. If the chain posts no root within about 30 days of registration, anyone can reclaim it and the deposit goes to the treasury.

Rome completed the v0.3.0 devnet guide on a chain of its own, from registration to the deposit refund. These are transactions from that run:

| Step                                        | Solana devnet transaction                                                                                                                                   |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A proved root, final when posted            | [`64uUhxFu…MpDGFr`](https://explorer.solana.com/tx/64uUhxFu9C48mqYd9rZTcfbEwvTB7C4PmSFWkY6feB8qsqJZNMh3gM8ZKM6woSsyjRgntqujSsHuCB2TGYMpDGFr?cluster=devnet) |
| Exit configuration activated                | [`2yyQrpv9…h1aSds`](https://explorer.solana.com/tx/2yyQrpv9CHQg7n5exxwERNYMEPDcm9gaPZje4jAv2Y2HHQ9DsPjbCDRbeLWoCZMwVv5UA6rohtJeQ1KZXRh1aSds?cluster=devnet) |
| Deposit queue opened                        | [`4tpfJKTV…XJbb9F`](https://explorer.solana.com/tx/4tpfJKTVmhvUnhH822JficbJrDJZHknBJ9rwmux5R17HLj6WGniuRCXd9wQ8jSzjVa9uTDm4g3f369QQnkXJbb9F?cluster=devnet) |
| 0.1 SOL deposited and credited in block 191 | [`5jKNqid8…VjYy67`](https://explorer.solana.com/tx/5jKNqid8XsqMedKBgm6dM5cSnszA7yFxa9xgm4MyGE45RcxthTkEZ9k1yGpzaPzqKGhN7CDz7iLAMMNFrfVjYy67?cluster=devnet) |
| A 0.03 SOL withdrawal proved                | [`3p8RhJJU…3rNDNz`](https://explorer.solana.com/tx/3p8RhJJUrvTxKrmtCJM1uZe2K6oLMw7cASoNQTm4RrMw6s1SrFeXWvws1D74UhnnRVPoevfhHmyNvkPGca3rNDNz?cluster=devnet) |
| That withdrawal paid to its recipient       | [`3dMpiQRX…nSVAz9`](https://explorer.solana.com/tx/3dMpiQRXgLBoNbBmKmz6qBVXfdX7EC9YwajQ35uGg1fzSb9HryH3awqrqx1fAEpsGfQPEDQrwUepgGtEkrnSVAz9?cluster=devnet) |
| Registration deposit refunded               | [`3YgyfLL2…zQk7wp`](https://explorer.solana.com/tx/3YgyfLL2bze6uefnUx3x76etTAu9WxQ3L2S8oNkKdSc3bBY7aHfWTZtrs1qDKxiJTMwA6i3LxRYTBC9n3RzQk7wp?cluster=devnet) |

The withdrawal started on the chain in block 192 and was paid out on Solana about four minutes later. Most of that time was spent closing and proving its batch. The exit prover then proved the withdrawal and sent its payout from the chain's vault. [Deposits and withdrawals](/rome-zk-evm/deposits-and-withdrawals) explains the limits and when a small payout needs a manual release.

## Limits of this release

This is a devnet release. Rome holds the upgrade authority for the shared programs and can upgrade them without notice. Check the [trust model](/rome-zk-evm/trust-model) before you put funds in a chain's vault.

The default setup does not close old inbox batches, their chunks or pending-root accounts. Their rent stays locked. A pending-account close setting exists, but it is off by default because older final roots may be needed for exits. Nothing in v0.3.0 sends the inbox's `CloseBatch` instruction. That also keeps deposit-record rent locked until the batch authority closes the crediting batch. Rome has not decided how long old batch data must remain on Solana.

Users rely on the operator to keep the sequencer running, post batches and send proofs. A valid proof does not post itself; the chain authority must sign the transaction that posts it. A chain without a prover has no final root, including at genesis, and cannot complete withdrawals. Read [How it works](/rome-zk-evm/how-it-works) for the path from a block to a final root.

## Code and help

The [v0.3.0 repository README](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/README.md) links the component docs, operator runbook and release notes. For questions, use [GitHub Discussions](https://github.com/rome-protocol/solana-zk-evm/discussions). For bugs or a verification-key request, use the [issue forms](https://github.com/rome-protocol/solana-zk-evm/issues/new/choose). For a security problem, follow [SECURITY.md](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/SECURITY.md) instead of opening a public issue.


# How it works

Follow an EVM block through Solana data posting, ZisK proving and final settlement.

Your sequencer accepts EVM transactions through JSON-RPC. It checks the signature, nonce, funds and gas, then executes accepted transactions in arrival order. It writes signed sub-blocks and their transactions to a durable ordered log before returning a pre-confirmation. The sequencer embeds reth to produce the chain's blocks.

A pre-confirmation tells you what the sequencer accepted and ordered. It is not a final Solana root. The rest of the path publishes the data and proves that executing it gives the claimed state.

```
EVM transaction -> sequencer -> ordered log -> batcher -> Solana inbox
Solana inbox -> derive -> reth-verifier -> blocks + witnesses
blocks + witnesses -> prover (ZisK batch guest) -> proof + root
proof + root -> Solana settlement (PostRootProved) -> final root
```

## From blocks to an inbox batch

The batcher reads consecutive blocks from the ordered log. The example `chain.toml` closes a batch at 60 blocks, or 60 seconds after its first block, whichever comes first. It compresses the batch into frames, posts the frames as inbox chunks on Solana, then finalizes the batch's Merkle commitment.

The inbox commitment covers the chain and batch IDs, the opening slot, the chunk count, the Merkle root of the chunks and the batch's deposit range. Finalizing an inbox batch means its data has been posted. It does not make its state root final. The chain authority signs the batch finalization.

The batcher cannot skip a block number. Settlement also takes the next batch in order. That is why a stuck or abandoned batch blocks later roots. The batcher resumes a partly written batch under the same number after a restart when it can reconstruct it.

## Derive, verify and prove

`derive` reads finalized inbox batches from Solana. It reconstructs the blocks and drives a separate `reth-verifier` node to execute them again. This node serves the execution witnesses the batch prover needs and the state proofs the exit prover needs. It runs reth's testing API, so it must stay off the public network.

The prover reads each finalized inbox batch, obtains blocks and witnesses from the verifier node, and runs the chain's batch guest in ZisK. The guest checks that the blocks match the inbox data and execute correctly. It commits the batch's public values, including the resulting state root. Your genesis and fee recipient are compiled into this guest, so each chain has its own guest and `programVK`.

The prover checks the proof locally, then sends `PostRootProved`. The settlement program checks the active verification key registered for this chain and the ZisK release named by that key. It also checks the proof's public values against the chain ID, block range, predecessor, inbox commitment and claimed root.

Veritas is Rome's verifier for ZisK PLONK proofs. Settlement links its verifier code and runs it inside `PostRootProved`; it does not call the separately deployed Veritas program. A valid proof writes a `Final` root and advances the finality head in the same Solana transaction. The genesis root of a permissionless chain is not final by itself. Its first final root needs a proved batch.

## Funds crossing the boundary

A deposit starts on Solana. The bridge program moves the deposited token from the depositor into that chain's vault and appends a numbered deposit record. The sequencer reads finalized Solana deposits and credits the named EVM address in order. The batcher commits the deposit range with the batch. The balance appears on the chain when its block seals, but its credit is final only when that batch's root is proved on Solana.

A withdrawal starts on the chain at its exit portal. The portal records a message and burns the native asset. The exit prover watches for that message and obtains an Ethereum storage proof from the verifier node against a final root. It checks that proof locally, then sends `ProveExit` to Solana settlement.

Settlement checks the portal, message, final root and exit cap, and prevents the same exit from being proved twice. The exit prover then sends the bridge's release transaction. The bridge consumes the proved exit and pays the recipient from that chain's vault in one atomic transaction. The recipient comes from the proved record, so the person sending the release cannot change it.

Read the full [v0.3.0 architecture](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/ARCHITECTURE.md) for wire formats, component boundaries and the checks on each instruction.


# Run a rollup

Register and run your own Rome ZK EVM chain on Solana devnet with the v0.3.0 rollup runner.

The `./rollup` runner starts one chain against Rome's shared Solana devnet programs. You run its node and pay its Solana costs. You do not deploy the settlement programs yourself. Registration is permissionless, but proved roots need a chain-specific verification key registered by Rome.

This page uses **v0.3.0**. The [full devnet guide](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/RUN-ON-DEVNET.md) covers every setting, failure and recovery step.

## Before you start

Use an x86-64 Linux machine with bash 4 or newer, Git, Docker Engine 28 or newer with Compose, curl, jq, openssl, the Solana CLI and Python 3.11 or newer. You need a Solana devnet RPC endpoint, a funded Solana payer keypair, which is also your chain authority, and a sequencer signing key. Keep both files private and readable by container user 999. The guide explains how to make them.

Budget about **7 SOL on devnet** for the payer. Registration locks 5 SOL; `./rollup check` expects at least 1 SOL left for posting and rent. The deposit becomes refundable after the first final root or ten posted roots. If the chain is reclaimed before then, the deposit goes to the treasury. The chain also needs an EVM fee-recipient address you control.

To settle roots, you need a prover host with one NVIDIA GPU with more than 30 GB of memory. A 24 GB card is too small for the final proof step. [Prover host](/rome-zk-evm/prover-host) gives the RAM and disk budget. The node can run and publish batches before the prover is ready, but none of its roots will be final.

## Start the node

Clone the release and work from its rollup directory:

```sh
git clone --branch v0.3.0 https://github.com/rome-protocol/solana-zk-evm.git
cd solana-zk-evm/deploy/rollup
cp .env.example .env
cp chain.toml.example chain.toml
```

Set the Solana RPC, signer locations and `ROME_ZK_TAG=v0.3.0` in `.env`. Keep `PROGRAMS_JSON=programs.devnet.json`. Set `ROME_ZK_GUEST_TAG=v0.3.0` and `ZISK_RELEASE=1.3.1-alpha` for the later guest build. Keep the RPC bound to loopback for a local first run. Leave `PROVER` unset until the guest and its key are ready.

Set `genesis.fee_recipient` in `chain.toml` to your EVM address. Do not choose a chain ID. Settlement derives one from the payer and its registration count; `init` reads it before sending anything. About half of all IDs are too large for MetaMask. If yours is one, `init` stops with `ChainIdNotWalletSafe`; make a new payer key, point `PAYER_KEYPAIR_PATH` at it and run `init` again.

A new chain starts with zero balances. If you declare one backed balance to get gas before deposits open, lock the same amount of wrapped SOL in the chain's vault before requesting the verification key.

```sh
./rollup init
./rollup register --dry-run
./rollup register --confirm
./rollup up
./rollup check
```

`init` renders the genesis and service configuration. `register --confirm` locks the 5 SOL deposit and creates the batch cursor. `up` starts the sequencer, batcher, derive and verifier node. `check` names any missing service, then compares the chain ID, the heads, the batcher's progress, the inbox and the roots. Run it again after batches have had time to close; its first batch-progress check only records a sample.

Without a prover, the sequencer serves RPC, the batcher posts transaction data to the inbox, and derive reconstructs the chain. An inbox batch is not a final settlement root. Even the genesis root waits for a proof.

## Move from batches to final roots

Build the guest for your chain's genesis:

```sh
./rollup guest-build
```

The build uses the v0.3.0 guest and ZisK 1.3.1-alpha. It produces the chain's guest program and verification-key metadata. Computing `programVK` needs the ZisK proving keys and about 36 GB of RAM. Registering the chain does not register a verification key. Send the [verification-key request](/rome-zk-evm/verification-key) after the build. Rome checks your genesis and guest, then registers the key with an activation slot.

On the prover host, set `PROVER=on` and the guest, verification metadata and ZisK settings described in the full runbook. After the key is active:

```sh
./rollup init
./rollup up
./rollup check
```

Check that the `service prover` and `verification key` items pass. The prover works through inbox batches in order. Each valid proof posts a root that is final as soon as its Solana transaction lands.

## Make the first block

With no backed genesis balance, the chain has no gas for a transaction until deposits open. Deposits need a first proved root. The default `empty_block_interval_secs = 0` leaves an idle chain without blocks, so set it to `60` under `[profile]` in `chain.toml` after the key is active and the prover is running. Render and restart:

```sh
./rollup init
./rollup up
./rollup check
```

Once the first root is final, set the interval back to `0` and rerun `init` and `up` unless you want regular empty blocks. Turning them on while waiting for Rome to register the key creates a backlog. Every batch must be proved in order.

With a 60-second empty-block interval and the 60-second batch close, the chain makes about one batch a minute, or about 1,440 a day. On devnet, a proved root cost the payer about **0.003 SOL**: about 0.002 SOL in pending-account rent, the 0.001 SOL settlement fee and a transaction fee on top. That cadence is about 4.4 SOL a day before inbox posting costs. Pending-account rent stays locked with the default settings. With empty blocks off, an idle chain seals no batches and pays no posting costs.

To accept deposits, next create the vault, activate exit configuration after its delay, and open a deposit queue. Those commands and the checks are in [Deposits and withdrawals](/rome-zk-evm/deposits-and-withdrawals).


# Verification key

Build a chain-specific batch guest and request registration of its ZisK verification key.

Each chain has its own batch guest. The guest contains the chain's genesis and fee recipient. It checks the batch data and execution, then commits the public values that Solana settlement checks. The guest's `programVK` identifies the program being proved.

A permissionless chain registers with an empty verification-key registry. It cannot post a final root until Rome adds a key for that chain. The chain authority cannot include a key in its registration transaction. This is the v0.3.0 process on Solana devnet.

## Build the guest

From `deploy/rollup` in the v0.3.0 checkout, use the release settings `ROME_ZK_TAG=v0.3.0`, `ROME_ZK_GUEST_TAG=v0.3.0` and `ZISK_RELEASE=1.3.1-alpha`. Run `./rollup init` first so the genesis and chain ID are rendered. You need the chain registered before you send the request. Then run:

```sh
./rollup guest-build
```

The command builds an image with the ZisK toolchain, clones the rollup and guest sources at those tags, builds the guest from your genesis and checks that it carries your chain ID. It prints the guest ELF's SHA-256. The generated metadata records that hash, the genesis hash, the chain ID, the ZisK release and its scheme number. For this release those last two values are `1.3.1-alpha` and `2`.

To compute `programVK` locally, the build needs the matching ZisK proving keys and about 36 GB of RAM. That step takes about 40 seconds. If you skip that step or do not have the keys, the build reports `ProgramVkNotComputed`. The prover refuses metadata without `programVK`; compute it with the matching keys or ask Rome for it before you start proving.

Two machines building the same genesis at these tags get the same ELF hash. A changed genesis means a different guest, and genesis cannot be changed after chain registration. The build refuses a genesis whose chain ID differs from your chain's ID (`ChainIdMismatch`) and a genesis that gives more than one account a balance (`GenesisFundedAccountLimit`).

## Ask Rome to register it

Use the [verification-key request form](https://github.com/rome-protocol/solana-zk-evm/issues/new?template=vkey-request.yml). Have your chain ID, the chain authority's public key and the signature of the transaction that registered the chain ready. Include the guest tag and the generated metadata: ELF hash, genesis hash, `programVK`, `zisk` and `scheme`. Attach or paste the rendered genesis. If genesis declares a backed balance, have the matching funded vault ready. Include a `./rollup check` result that passes except for items waiting on the key.

Rome rebuilds the guest from the tag and genesis. It checks the chain ID, genesis, ELF hash and `programVK` against your request, then registers the key under the matching ZisK release. Rome replies on the issue with the signature of the key registration transaction. The key becomes usable at its recorded activation slot; run `./rollup check` after that slot. With the prover on, the `verification key` check should pass. The deposit-capable key check should pass too.

Rome registers a **complete request within two business days**. If a request is incomplete, Rome replies with what is missing. The two business days start when the request is complete.

The timing matters because the reclaim window starts when you register the chain, not when its key becomes active. The v0.3.0 devnet window is 6,480,000 slots, about 30 days. If no root is posted when that window ends, anyone can reclaim the chain and its 5 SOL deposit goes to the treasury. Rome's registry authority can change the global window, subject to a 216,000-slot minimum.

After activation, give the prover the matching guest, metadata and ZisK proving keys, turn `PROVER=on`, then run `./rollup init`, `./rollup up` and `./rollup check`. The proof must use an active key registered for this chain. Moving later to a different ZisK release needs another guest build and another registered entry; see [Releases](/rome-zk-evm/releases).

The full request fields and operator steps are in [Run on devnet at v0.3.0](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/RUN-ON-DEVNET.md).


# Prover host

Size and prepare the GPU host that proves Rome ZK EVM batches with ZisK 1.3.1-alpha.

The node can sequence transactions and post batches without a GPU. It needs a prover to make any root final on Solana. In v0.3.0 the prover runs ZisK 1.3.1-alpha with the guest built for your chain's genesis. It proves inbox batches one at a time, checks each proof locally, then posts it to settlement.

The [full prover-host guide](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/PROVER-HOST.md) has the setup and recovery steps. Use it with the [devnet runbook](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/RUN-ON-DEVNET.md).

## Size the host

Use x86-64 Ubuntu 22.04 or later and one NVIDIA GPU with more than 30 GB of memory. The setup script accepts a GPU with about 32.2 GB or more. The final proof step asks for about 30 GB, so a 24 GB card is too small. The NVIDIA driver must be at least 525.60.13.

Allow at least **128 GB of RAM** and **125 GB free disk** at the ZisK install location. The 1.3.1-alpha proving-key archive is 5.1 GB and the PLONK-key archive is 21.9 GB. After setup generated its constant files, the two key directories measured 21 GiB and 26 GiB. The first proof adds cache files. A CPU host measured 102.1 GB total after one proof, and GPU cache growth has not been measured.

The 1.3.1-alpha proving library reported 24.7 GB for its main working buffer and 5.6 GB for recursion on a batch of about 3 million steps. The whole-process peak on this release was not recorded. The devnet GPU run used a host with 176 GB of RAM; its peak RAM use was not recorded either. Use 128 GB as a sizing estimate, not a measured peak.

## What was measured

One nearly empty batch took about **17 seconds**, including the PLONK wrap, on a host with one **96 GB RTX PRO 6000**, 48 CPU cores and 176 GB of RAM. About 95 GB of GPU memory showed as in use. That is what this run held, not the minimum GPU memory for every batch. Larger batches have not been timed on a GPU.

Rome cleared 116 nearly empty devnet batches in about 35 minutes on that GPU. That matters when a chain has produced empty blocks while waiting for its verification key: the prover must work through them in order before it can reach newer state.

## Run the setup script

Install the NVIDIA driver and reboot if needed. From the repository root, run the release's script as root. Set `ZISK_HOME` to the directory where ZisK and its keys will live. Use the same value in your rollup `.env`:

```sh
sudo ZISK_HOME=/opt/zisk bash deploy/rollup/prover/setup-prover-host.sh
sudo ZISK_HOME=/opt/zisk bash deploy/rollup/prover/setup-prover-host.sh --check
```

The script installs the ZisK GPU build, its proving and PLONK keys, the system libraries it needs, and Docker's NVIDIA runtime. It sets the locked-memory limit to unlimited. Log out and back in so that limit applies to your shell. It then checks the GPU and driver, installed binaries, keys, memory limit and container runtime.

Before running the installer, it checks the installer's SHA-256. It checks both key archives before unpacking either one, and checks the installed `cargo-zisk` and `cargo-zisk-dev` GPU binaries before the script runs them. It generates the constant files needed for aggregation and PLONK proving. If a run fails, run the script again. It skips the key download when the installed keys already match the pinned manifest.

The script does not check every part of the ZisK install. The installer fetches a binary bundle and runs its toolchain installer before the script checks the two installed GPU binaries. Other executables and source files in that bundle are not hash-checked by this script. The full guide states this limit and has the commands for checking each download separately.

## Connect it to your chain

The prover needs the matching guest ELF, its verification metadata, and the same 1.3.1-alpha proving keys. Set `PROVER=on` and the guest and ZisK settings in the rollup configuration, then run `./rollup init` and `./rollup up`. The first run builds the prover image from the v0.3.0 source. The node image alone does not contain the prover.

Before every start, the prover checks the installed proving keys against the pinned SHA-256 manifest. That check covers file names and contents. Generated constant-tree and GPU constant files are excluded because they differ by host. A mismatch stops the prover with `KeysShaMismatch`. Do not change the manifest just to make a mismatched install start; install the release that matches the chain's guest and registered key.

Run `./rollup check` after starting. It checks the prover service, the active verification key, and settlement and prover lag. The [monitoring guide](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/MONITORING.md) lists the local metrics and useful alert conditions.


# Deposits and withdrawals

Move SOL into a Rome ZK EVM chain and withdraw its native coin to a Solana wallet.

In the v0.3.0 devnet setup, the chain's native coin is backed by wrapped SOL in its Solana vault. A deposit locks wrapped SOL and credits the named EVM address. A withdrawal burns that coin on the chain and pays wrapped SOL from the vault to a Solana recipient. One gwei on the chain corresponds to one lamport of wrapped SOL.

The [full withdrawal guide](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/WITHDRAWALS.md) has the exact call, checks and refusal names. The [devnet guide](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/RUN-ON-DEVNET.md) covers deposit setup and the depositor command.

## Deposit SOL

The operator first needs a proved root, a wrapped SOL vault, an active exit configuration that names the bridge, and a deposit queue. Without these, a new chain cannot accept deposits. The chain's genesis starts with no balance unless its operator declared and backed one balance.

A depositor uses `./rollup deposit` with an amount in lamports, an EVM recipient, their own Solana signer and `--wrap-sol`. That option wraps the SOL in the same Solana transaction as the deposit. The command is a dry run until `--confirm` is added. The default minimum is 1,000,000 lamports, or 0.001 SOL. The default queue fee is 100,000 lamports, or 0.0001 SOL, paid to the chain authority. The depositor also pays the transaction fee and rent for the deposit record.

The bridge locks the wrapped SOL in that chain's vault and appends a numbered record. The sequencer reads finalized Solana deposits and credits them in order in new EVM blocks. The balance appears at the chain RPC when its block seals. The credit is final only when the batch containing it is proved and its root lands on Solana.

The default queue inclusion deadline is 12 hours, with at most 256 deposits per batch and 4 per block. The inbox checks the deadline when a batch finalizes. That rule does not force the operator to produce or prove a batch. The operator can still delay a deposit by stopping the chain.

Deposit-record rent can return to the depositor after the crediting batch's root is final and the batch authority sends `CloseBatch`. Then anyone can send `CloseDeposit`. The deposit remains in the vault as backing. The v0.3.0 runner does not send `CloseBatch`, so records do not close automatically and their rent stays locked.

## Withdraw to Solana

Withdrawals are for the chain's native coin only. Send a payable `initiateExit(bytes32 solRecipient)` call to the exit portal at `0x4200000000000000000000000000000000000016`. The value is the amount to withdraw. The recipient is a **Solana wallet address encoded as 32 bytes**, not the address of its token account. Check that address before sending. The coins are burned as soon as the transaction lands, and nothing in the portal can give them back. Keep the message hash from the `ExitInitiated` event.

Wait for a proved root covering that transaction. The exit prover uses the verifier node to make a storage proof of the portal's message against that final root. Solana settlement checks the proof, the active exit configuration, the portal and the withdrawal cap. The exit prover then sends a release. The bridge takes the amount from the chain's vault and pays the recipient's wrapped SOL associated token account. It can create that token account when needed.

The payout follows the proof without a separate waiting period. The release transaction reads the recipient from the proved record, so the operator or any other sender cannot redirect it. The program consumes the record and pays out atomically, preventing a second payout for the same withdrawal.

Most of the wait is the time to close, post and prove the batch. The exit prover polls every two seconds and pays soon after a final root is available. In Rome's devnet run, a withdrawal started in block 192 was paid out on Solana about four minutes later. This is one measurement, not a fixed withdrawal time.

An exit cap limits how much can leave in each 172,800-slot challenge window. An exit that does not fit in the current window waits for the next one. A single exit larger than the whole cap needs the chain authority to raise the cap. Exit configuration changes take at least one challenge window to become active.

A payout can still wait after proof if the recipient lacks a wrapped SOL token account and the amount is below the default 0.01 SOL threshold. The exit prover leaves it for a manual release rather than pay to create that account. The user or operator can use `./rollup release-exit --message-hash` with the message hash and `--confirm`. Anyone can release a valid exit.

## What the operator runs

Create and fund the wrapped SOL vault after registration. For a backed genesis balance, lock that same amount before requesting the verification key. After the first proved root, set an exit portal, bridge and cap with `./rollup exit-config propose`. Wait at least one challenge window, then run `./rollup exit-config activate --confirm`. The chain authority opens the deposit queue with `./rollup deposit-queue init --confirm`.

Set `EXITS=on`, then run `./rollup init` and `./rollup up` to start the exit prover. It needs its **own funded payer**, separate from the batcher's payer. Each exit proof costs its payer a Solana fee and record rent. The runner rejects a missing or shared exit payer. Automatic payout is on by default; the exit prover also retries proved records left open after a restart.

Run `./rollup check`. It checks vault backing, deposit caps and backlog, the active bridge and exit configuration, the exit prover, stuck exits, its log scan and its payer balance. The default exit-payer floor is 0.1 SOL. Use `./rollup vault show` and `./rollup deposit-queue show` to inspect funds and queue state. The full guides above give the commands for funding a vault and changing queue settings.


# Trust model

Who can change a Rome ZK EVM chain on devnet and what depositors and withdrawers must trust.

The shared v0.3.0 programs on Solana devnet are upgradeable. A proved root is final under the rules those programs enforce when it posts. Operators and users should also account for who can change those rules, who controls each chain, and who can stop publishing data.

Read the [full v0.3.0 devnet trust model](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/TRUST-MODEL.md) for the exact powers of each authority and the checks in the current programs.

## Rome's program and registry authorities

Rome holds the upgrade authority for the shared inbox, settlement, Veritas and bridge programs. It is a single-signature authority on devnet. An upgrade can take effect without a notice period and can change any current program rule. Treat this as a devnet trust assumption when depositing funds.

Rome also holds the settlement registry authority. Under the current programs, it can turn permissionless registration on or off, change registration deposits for new chains, change settlement fees even for an existing chain without a protocol cap, choose the treasury, change allowed timestamp drift and change the global reclaim window. The reclaim window can be shortened for registered chains, but not below 216,000 slots under the current program.

The registry authority registers, retires and changes the activation slot of a chain's verification keys. A proof counts only against an active key for that chain. This authority can therefore change which guest's proofs can finalize new roots or leave a chain with no usable key. A new entry can be active as soon as its registration transaction lands. Already-final roots stay final when a key is retired.

Without upgrading the programs, Rome's keys cannot create a vault for a chain whose authority Rome does not hold, change that vault's mint, choose where its payouts go, or withdraw from it without a proved exit. Program upgrade authority can change those rules, so users must trust Rome not to make such an upgrade on devnet.

## Your chain authority

In the `./rollup` setup, the Solana payer is also the chain authority. It signs inbox batch publication and finalization, proved-root posting, and chain configuration. It can close eligible batch, chunk and pending-root accounts to recover their rent. There is no instruction to rotate this authority, so losing it stops those signed actions.

Registration fixes the chain's ID, genesis, inbox program and challenge-window length. The authority cannot edit the registered genesis. It chooses a fee recipient in that genesis. A new chain normally starts with zero balances; any declared starting balance needs matching vault backing before Rome registers its verification key. Settlement does not itself check that backing.

The authority can propose a different exit portal, bridge program, withdrawal cap and poster bond. A proposal becomes active no sooner than one chain challenge window later. Anyone may activate it after that slot. Users should inspect the active exit configuration before withdrawing.

After the first proved batch, the authority can create a deposit queue and choose its inclusion deadline, per-batch and per-block limits, minimum deposit, fee and fee recipient. Changes also have a delayed activation slot. The current program constrains which waiting deposits a **finalized** batch may leave out. It does not force the operator to produce that batch.

## What a user relies on

A depositor trusts the operator to keep the sequencer running, include the queued credit in a batch, post the batch and prove its root. The operator can choose EVM transaction order and refuse transactions; there is no general forced-inclusion lane for EVM transactions. A deposit balance can appear at the chain RPC before its root is final on Solana.

A withdrawer needs a final root covering the exit message, an active exit configuration and enough wrapped SOL in the chain vault. The operator can delay that root by stopping batches or proofs. They can delay payouts by stopping the exit prover or leaving the vault or exit payer unfunded. Anyone can submit a valid exit proof and release, so the operator's exit prover is not the only path once the data and final root are available.

Under the current programs, settlement binds an exit proof to the configured portal and a final root. It limits exits per challenge window and prevents replay. The bridge reads the recipient from the proved record and checks the recipient's associated token account. Whoever sends the release cannot redirect its payout. A vault without enough funds cannot pay, even if the exit proof is valid.

The 5 SOL registration deposit is a chain-registration bond, not backing for users' balances. It becomes refundable to the chain authority after the first final root or ten posted roots. If no root is posted before the reclaim window ends, anyone can reclaim the chain and its unrefunded deposit goes to the treasury. The devnet window at v0.3.0 is 6,480,000 slots, about 30 days, measured from registration.


# Releases

What v0.3.0 adds and how a chain moves between supported ZisK proof releases.

This section documents **Rome ZK EVM v0.3.0**. Use the matching v0.3.0 source, node image and batch guest. The guest uses ZisK 1.3.1-alpha. The release tag names the source tree used for the image. The `main` branch may have changed since the release.

The [full v0.3.0 changelog](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/CHANGELOG.md#v030) records the commands, fixes and limits behind the release. The [ZisK release policy](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/docs/ZISK-RELEASES.md) describes the on-chain registry and move process.

## In v0.3.0

Devnet chains can now take deposits and pay withdrawals from their own wrapped SOL vaults. The chain authority can open a deposit queue, set its bounds and fees, and change them after a delayed activation. The sequencer credits finalized Solana deposits in EVM blocks; the batcher commits their ordered range to the inbox batch.

The node image includes an exit prover. With `EXITS=on`, it watches the chain's exit portal, proves withdrawals against final Solana roots, and sends the vault release itself. It uses its own payer. A small payout to a recipient without a wrapped SOL token account waits for manual release by default. `./rollup check` reports exit configuration, service health, stuck exits and exit-payer balance.

Proofs use ZisK 1.3.1-alpha. Settlement and Veritas now keep a verifying key and pinned recursion root per ZisK release. The prover host setup downloads and checks this release's key archives, checks the GPU binaries and prepares the constant files used by aggregation. The release also adds the verification-key request and bug-report issue forms.

The devnet guide links the explorer transactions from Rome's own run, from registration to payout. This release still leaves old inbox batch and chunk accounts open. Pending-root accounts remain open by default; their rent accumulates. Rome has not yet decided how long old batch data must stay on Solana.

## ZisK releases belong to each chain's key

Each chain has a verification-key registry. An entry holds `programVK`, a public-values layout, an activation slot and a scheme number. That number names the ZisK release used to build the guest and key; the proof does not choose its release. The registry has four entry slots.

| Scheme | ZisK release  | v0.3.0 status                              |
| ------ | ------------- | ------------------------------------------ |
| `1`    | `1.2.0-alpha` | Withdrawn; its proofs are refused.         |
| `2`    | `1.3.1-alpha` | Open; new entries and proofs are accepted. |

Settlement looks up the active entry matching a proof's `programVK`. The entry selects that release's Veritas key and pinned recursion root. A proof with the wrong root is refused before the public-values and pairing checks. A withdrawn release has no verifying key in the production build.

## Move a chain to a new release

Install the new ZisK toolchain and proving keys while keeping the old install available. Set the rollup source tag, guest tag and ZisK release together, then run `./rollup guest-build` to build a new guest and `programVK` for the same genesis. The prover needs the matching ZisK install, guest and verification metadata.

Send Rome the chain's genesis, guest tag, ELF hash and `programVK`. Rome rebuilds and checks the guest and registers the new key with a future activation slot. If the chain has posted a root, Rome also needs the previous guest and release. Check the new on-chain entry and activation slot, then switch the prover when that slot arrives. Rome retires the old entry after the switch. A future entry cannot verify early.

Rome's support policy keeps at most two ZisK releases open at once. A closing release accepts no new entries, but existing entries can still verify while chains move. When Rome announces that a release is closing, chains on devnet and testnet have 30 days to move. That window is Rome's support policy, not an on-chain timer. Rome may retire entries at once for a security withdrawal. A withdrawn release accepts neither new entries nor proofs, so a chain still on 1.2.0-alpha needs an active 1.3.1-alpha entry before it can post another root.


# Overview

Ostia is a Canton zkEVM whose blocks and Canton actions settle in one transaction.

Ostia is a Canton zkEVM, with chain id **770101** and the test coin **tROME**. Canton orders its EVM blocks. A zero-knowledge virtual machine, ZisK, proves each block before it can commit. One Canton transaction carries the block, its proof and the Canton actions it needs. They settle together, or nothing commits.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [repository README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/README.md).

The gateway contract records each block's requests to Canton as legs. A deposit locks a Canton token and mints its wrapped form on the EVM. A withdrawal burns the wrapped form and releases the Canton token. A payment through the gateway executes the Canton allocation that the parties agreed to use for that payment. [How it works](/canton-zk-evm/how-it-works) traces a block through the required checks.

For the EVM rollup that settles on Solana, see [Rome ZK EVM](/rome-zk-evm/zk-evm).

## What the demo showed

The demo passed its checks on **2026-10-05**, on a local Canton network with one NVIDIA RTX PRO 6000 Blackwell Server Edition GPU. The runs used commit `6b445073163832e9023850f53678617d90dfc808` of `canton-zk-evm`. TKA was the EVM test token. TKB was the Canton test token, and wTKB was its wrapped form on the EVM. U and V were the test parties.

An empty smoke block committed first. A setup block then deployed TKA and created wTKB. The five runs followed:

| Run                      | What happened                                                                                                    | Outcome                                                                                                                                    | Proof time                                                              |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| 1: deposit               | U deposited 10 TKB and claimed 10 wTKB.                                                                          | Block 3's record and the gateway's new 10 TKB holding came from one Canton update.                                                         | 6.72 s                                                                  |
| 2: payment               | V paid U 10 TKA through the gateway against U's allocation of 10 TKB to V. V also sent U a plain 1-TKA transfer. | Block 4's record and V's new 10 TKB holding came from one update. The plain transfer made no leg.                                          | 6.72 s                                                                  |
| 3: withdrawal            | U burnt 4 wTKB and received 4 TKB.                                                                               | Block 5's record and U's new holding came from one update. Wrapped supply and gateway custody fell to 6.                                   | 6.72 s                                                                  |
| 4: allocation taken back | U took back the payment's allocation after the proof was made.                                                   | Canton refused block 6. Resubmitting it without its leg also failed. The next build left out the payment and committed V's approval alone. | 6.75 s for the initial build; the second proof was not timed separately |
| 5: tampered proof        | One hex digit of the proof was changed.                                                                          | Canton refused block 7 after the sidecar rejected the proof. The head stayed at 6.                                                         | 6.74 s before tampering                                                 |

The wrapped proof was **1,344 bytes**. The six recorded proof times for the setup and five runs were **6.71 to 6.75 s**. The empty block's proof took **6.31 s**. These were individual runs with the prover already running. The recorded times are not a benchmark and do not measure block throughput.

The [full demo results](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md#results) explain each check. The [results folder](https://github.com/rome-protocol/canton-zk-evm/tree/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/results) holds the records and proofs. [Measurements](/canton-zk-evm/measurements) separates proof times, builder times and the earlier prover benchmark.

## Limits of this release

Ostia is an early release. It runs on a local test network with one builder, one operator and one confirmer. The network loses its state when restarted; it does not provide a persistent chain between runs.

The code and ZisK **1.3.1-alpha** have not been audited. tROME has no value. Do not use this release for anything of value.

The gateway party holds the Canton tokens backing their wrapped forms. That party is hosted on the operator's participant. Whoever runs that participant can move the locked tokens.

The network uses pinned Canton 3.6 snapshots and a snapshot Daml compiler for the external call. The exact versions are in [PINS](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/PINS). Read [Trust model and limits](/canton-zk-evm/trust-model) before running it.

## Run it

[Run it locally](/canton-zk-evm/run-it-locally) covers the tests, the rehearsal without a GPU and the GPU demo. [Prover host](/canton-zk-evm/prover-host) describes the tested hardware and prover setup. The [Explorer](/canton-zk-evm/explorer) shows EVM blocks beside their Canton records and can check a block's proof in the browser.


# How it works

Follow one Ostia block from reth through proving to its Canton commit.

Each builder run makes, proves and submits one block. Canton decides whether that block becomes the chain's next head. An EVM block and the Canton legs it recorded commit in the same transaction.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [design](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/docs/DESIGN.md).

```mermaid
flowchart TD
    W["EVM transactions"] --> R["reth builds on demand"]
    R --> B["Builder reads gateway legs and finds their Canton contracts"]
    B -->|"A leg cannot settle: leave its transaction out and rebuild"| R
    B -->|"Every leg can settle"| P["ZisK proves the block"]
    P --> A["Advance: block, proof and legs in one Canton transaction"]
    A --> S["External call: sidecars check proof and gateway state"]
    S --> D["Daml checks the head and settles the legs"]
    D --> C["Canton commits the block and its legs together"]
    C --> F["reth marks the block final"]
```

## Build on Canton's head

Wallets send EVM transactions to reth. The supplied launcher keeps peer discovery off. reth makes a block when the builder asks for one.

The builder first reads the active `ZkChain` contract on Canton. It moves reth's head, safe block and finalized block to that committed head. It chooses pending transactions by price, keeping each sender's nonce order and staying within the gas cap. It asks reth to build with that explicit transaction list.

An EVM transaction that requires a Canton action calls the gateway. The gateway records a leg and folds it into a running hash for the block. The builder reads the gateway's events in log order and finds the Canton contracts needed by each leg.

If an allocation is gone, a receiver has not accepted withdrawals, or another required contract is missing, the builder leaves out the transaction and that sender's later transactions. It returns reth to Canton's head and builds again. These checks happen before proving. The [builder README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/builder/README.md) lists the checks for each leg.

## Prove the block

The builder saves the block and reth's execution witness. The input tool combines the block, witness and genesis into the prover input. ZisK runs the guest, which validates the block under the chain's fixed rules.

The prover returns a 1,344-byte wrapped proof that commits to the block hash. The builder submits `Advance` with the header, transactions, proof, legs and the gateway's account and storage proofs at the block's state root.

## Check and settle in one transaction

Each confirming participant runs a sidecar. Canton's external call sends the block to that participant's sidecar for `verify`. It checks the proof, the pinned program key and ZisK release, the header's hash and the transaction list's root.

Daml checks the answer against `ZkChain`: the parent must be the current head, the number must be the next one, and the gas limit and gas used must fit the cap. Daml compares the sidecar's answers; it does not verify the zero-knowledge proof itself.

Daml then writes a line for each leg from the Canton contracts it names. A withdrawal's id, burner and amount come from the builder; the proven hash binds them to what the block recorded. The sidecar's `legs` function checks the gateway's account and storage proofs, hashes those lines in order and compares the result with the gateway's value in the proven state. A block with no legs must pass the same check with an empty list.

Daml settles the legs in order and creates the `BlockRecord` and the next `ZkChain`. Canton commits these changes together. If a check or settlement fails, nothing commits. Consuming the chain's one state contract makes Canton the authority on block order. See the full [Daml interface](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/daml/README.md) and [sidecar checks](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/sidecar/README.md).

## When Canton refuses

After Canton commits the block, the builder marks it final in reth. If Canton refuses it, the builder returns reth to Canton's last committed block and sends the refused block's transactions back to the pool. The next run checks their legs again. A payment whose allocation was taken back is left out.

If the builder loses the submission reply, it reads Canton's head again to determine whether the block committed. If that read also fails, it reports an unknown outcome and leaves reth where it is. The next run starts from Canton's head. These recovery rules are in the [builder README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/builder/README.md#what-one-run-does).


# Deposits, withdrawals and payments

How the gateway ties EVM deposits, withdrawals and payments to their Canton legs.

The gateway is an EVM contract included in the chain's genesis. It records what a block needs Canton to do. Each request is a leg. The block's proof commits to the gateway's state, including the hash of the legs in the order they were recorded.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [gateway README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/gateway/README.md) and [Daml interface](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/daml/README.md).

| Leg        | Gateway call                     | EVM action                                      | Canton action in `Advance`                                    |
| ---------- | -------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |
| Deposit    | `claim(id, token, amount)`       | Mint wrapped tokens to the caller.              | Execute the depositor's allocation to the gateway.            |
| Withdrawal | `withdraw(token, amount, party)` | Burn the caller's wrapped tokens.               | Pay the Canton token from gateway custody to the named party. |
| Payment    | `pay(id, token, to, amount)`     | Transfer an ERC-20 from the payer to the payee. | Execute the allocation named by the agreed payment terms.     |

## Deposit a Canton token

The depositor allocates a Canton token to the gateway, with the operator as executor. The depositor signs a `DepositRequest` naming the deposit id, the EVM address allowed to claim and the allocation.

That EVM address calls `claim`. The gateway mints to its caller and records the deposit leg. The recipient needs tROME for gas. The builder matches the leg to the request, the registered token and the allocation.

`Advance` executes the allocation into gateway custody and commits the block that minted the wrapped tokens. A wrapped token has 10 decimals, matching Canton amounts. The conversion between the two amounts is exact, with no rounding.

## Withdraw to a Canton party

The holder calls `withdraw` with the wrapped token, amount and full Canton party id. The gateway burns the wrapped tokens and records the receiver in the leg.

The receiver must have signed a `WithdrawalAcceptance`. It remains active until archived. The builder also needs enough unlocked gateway holdings of the registered instrument. It leaves the transaction out if these conditions do not hold.

`Advance` pays the receiver through the token registry's transfer factory. The Canton transfer and the block containing the burn commit together. The builder includes at most one withdrawal of an instrument per block, as described in the [builder README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/builder/README.md).

## Tie a payment to one allocation

U allocates a Canton token to V. U and V sign `DvpTerms` naming the allocation, payment id, EVM token, EVM payee and amount. The payment id is the SHA-256 of the UTF-8 text `<U's party id>,<V's party id>,<label>`, written in lowercase hex. The terms reject any other id.

V approves the gateway and calls `pay` with that id. The gateway moves the ERC-20 from V to the payee and records the payment leg. `Advance` uses the signed terms to execute the allocation from U to V.

A plain ERC-20 transfer records no leg. Even a transfer of the same token to the same payee in the same block cannot stand in for `pay`. The [design's payment flow](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/docs/DESIGN.md#the-flows) explains this binding.

## What the proof fixes

Each leg records its kind, id, token, account, amount and Canton party where applicable. The gateway hashes the legs in order. `Advance` checks that its attached legs produce the hash in the proven gateway state. A missing, extra, reordered or changed leg makes the transaction fail. The check also covers blocks with no legs.

Proving the block does not reserve its Canton allocation. The sender can still take the allocation back before settlement. If that happens after proving, Canton refuses `Advance` and reth returns to the last committed block. The same proven block cannot be resubmitted with its leg removed. The next build leaves out the payment, as demonstrated in [run 4](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md#results).

## Token and custody limits

The Canton tokens are held by the gateway party on the operator's participant. Whoever runs that participant can move them. Registering a token also means trusting its registry's Daml code, which runs with the gateway's authority.

Tokens sent directly to the gateway's EVM address cannot be recovered. If a token charges a transfer fee or otherwise transfers a different amount, the payment leg still records the requested amount, rather than the amount actually transferred. Read [Trust model and limits](/canton-zk-evm/trust-model).


# Run it locally

Run the checks, the rehearsal without a GPU and the local Ostia demo on a GPU.

The supplied scripts run a local test network. Canton holds its state in memory, so each restart discards that state and creates a new network. The GPU demo proves blocks. The rehearsal uses a stand-in proof and needs no GPU.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [network guide](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/network/README.md) and [demo guide](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md). Run the commands below from the `canton-zk-evm` repository root.

## What you need

| Work                     | Requirements                                                                                                                 |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| Repository checks        | Docker, `bash`, `curl`, `jq`, `openssl` and Python 3. Rust tests use `rust-toolchain.toml`.                                  |
| Canton integration tests | Linux x86-64, Java 21 and network access.                                                                                    |
| Demo rehearsal           | Linux x86-64, Docker, Java 21, Rust and Python 3.12 or newer.                                                                |
| Explorer tests           | Node 22.18 or newer.                                                                                                         |
| GPU network and demo     | Linux x86-64, an NVIDIA GPU with its driver installed, Docker, Rust, Python 3.12 or newer, `jq`, `curl`, `openssl` and ZisK. |

The [repository README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/README.md#running-the-tests) lists the test prerequisites. The GPU installer uses `sudo` and `apt` on Debian or Ubuntu and needs about 100 GB of free disk. It installs system packages and changes memory-lock limits. The network's tool fetcher installs Java 21 if missing. See [Prover host](/canton-zk-evm/prover-host) for the tested hardware; minimum GPU memory has not been established.

## Run the tests

Start with the quick checks from [CONTRIBUTING.md](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/CONTRIBUTING.md#building-and-testing):

```sh
tests/static.sh                  # pins, genesis and licence lines agree
tests/refuses_missing_flag.sh    # the reth launch refuses a missing or wrong flag
tests/fixtures.sh                # the recorded proof is consistent
gateway/build.sh --check         # the checked-in gateway code is what Gateway.sol compiles to (Docker)
```

The repository README also gives these checks. They need no GPU:

```sh
tests/udp_control.sh             # the UDP check fails on a container with a UDP listener (no reth)
tests/discovery.sh               # starts the pinned reth, checks it has no peers, stops it
tests/network_static.sh          # reth only through launch.sh, the Canton config, the recorded smoke result
tests/network_checks.sh          # the compiler pin check, the DAR's id, the stop on another programVK (no GPU)
tests/canton_network.sh          # the pinned Canton, the real Daml package and one Advance (Linux x86-64, Java 21, Python 3; no GPU)
gateway/tests/run.sh             # the gateway on a real reth, started with peer discovery off (Docker, curl, jq, Python 3 with the packages in demo/requirements.txt)
tests/guest-repro.sh             # the guest build runs in one fixed folder, so the source folder does not change the ELF (stand-ins for the ZisK tools; the same check on real ZisK passed on 2026-10-04, see guest/README.md)
tests/demo_static.sh             # the demo's pins, tamper switch and genesis override, and its recorded results (no GPU)
tests/demo_explorer_static.sh    # the explorer's images are pinned, and what its container is given (no Docker)
(cd explorer && npm ci && npm test)  # the explorer's tests, against fakes (Node 22.18+; CI uses EXPLORER_NODE_IMAGE in PINS)
(cd guest/rules && cargo test)   # the chain rules and their hash (needs Rust)
```

## Rehearse without a GPU

```sh
tests/demo_rehearsal.sh          # the demo and explorer with a stand-in proof (Linux x86-64, Docker, Java 21, Rust, Python 3.12+; no GPU)
```

This runs the real reth, Canton, Daml package, token standard and builder. The sidecar checks the gateway's legs against storage proofs, while a stand-in answers the proof's `verify` call. The rehearsal neither proves a block nor verifies a real zero-knowledge proof.

It also checks the explorer and faucet. The explorer's Verify check must reject the stand-in proof. The [demo README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md#the-rehearsal) describes the rehearsal's coverage.

## Start the local network on a GPU

Install ZisK with `prover/install.sh` before starting the network. Always start reth through the supplied launcher; `network/up.sh` uses it.

```sh
network/up.sh       # tools, guest build, reth, sidecars, prover, Canton, the chain
CZE_SOURCE_COMMIT=$(git rev-parse HEAD) network/smoke.sh    # one empty block, proven and committed
network/down.sh
```

`up.sh` builds the guest and refuses a program key, ZisK root or rules hash that differs from the recorded values. The network has one synchronizer and three participants. The operator and confirmer participants each run a sidecar. The participant named users hosts the test parties and the reader.

The smoke test commits an empty block, checks the chain's head and the reader's block record, and checks that reth marked it final. State and logs go to `state/`, or `CZE_STATE_DIR` when set.

## Run the five demo cases on a GPU

Use a new local network for the demo:

```sh
demo/prepare.sh
CZE_GENESIS_FILE=$PWD/state/demo/genesis.json network/up.sh
CZE_SOURCE_COMMIT=$(git rev-parse HEAD) network/smoke.sh      # optional: one empty block first
CZE_SOURCE_COMMIT=$(git rev-parse HEAD) demo/run.sh
network/down.sh
```

The preparation script creates test keys and a funded genesis for the run. The demo runs its setup block, deposit, payment, withdrawal, allocation refusal and tampered-proof refusal. The script stops if a check fails and writes its results to `demo/results/`.

For a source export without `.git`, set `CZE_SOURCE_COMMIT` to the full hash of the commit used for the export. To include the faucet, its funded genesis must be prepared before the network starts; see [Explorer](/canton-zk-evm/explorer) and the full demo guide.


# Prover host

The prover's host requirements, persistent worker and pinned guest build.

The prover runs ZisK with one GPU worker. The coordinator and worker remain running between requests, with the PLONK key already loaded. For each block, the builder sends input and receives a wrapped proof.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [prover README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/prover/README.md) and [measurements](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/docs/METRICS.md).

## Host requirements

The installer targets Linux x86-64 with `apt`, `sudo`, an installed NVIDIA driver and about 100 GB of free disk. It installs system packages and changes memory-lock limits.

The 2026-10-03 benchmark used one NVIDIA RTX PRO 6000 Blackwell Server Edition with 96 GB of GPU memory, 48 vCPUs on an AMD EPYC 9B45, and 176 GiB of host RAM. It ran Ubuntu 22.04.5 with driver 580.178.04. These specifications describe the tested machine; they do not establish the minimum hardware needed to run the prover.

The benchmark recorded 22.9 GiB of emulator shared memory. The PLONK wrap alone reported a GPU requirement of 30,054,767,337 bytes, about 28.0 GiB. Peak GPU memory use was 93.2 GiB, but the prover sizes a large buffer to the card. That peak reflects the buffer allocation and does not show how much GPU memory the whole prover needs at minimum. See [Measurements](/canton-zk-evm/measurements) for the scope of each number.

## Build and run

The scripts use the versions in [PINS](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/PINS). The prover uses ZisK 1.3.1-alpha, Node 20.20.2 and snarkjs 0.7.6. The explorer's Node version is separate.

| Script                  | What it does                                                                                    |
| ----------------------- | ----------------------------------------------------------------------------------------------- |
| `prover/install.sh`     | Check the installer's pinned checksum, then install the GPU build, proving key and PLONK setup. |
| `prover/build-guest.sh` | Build the guest ELF and print its SHA-256, programVK, rootC and rules hash.                     |
| `prover/start.sh`       | Start the coordinator and one GPU worker with the PLONK key loaded.                             |
| `prover/make-input.sh`  | Turn a saved block, execution witness and genesis into prover input.                            |
| `prover/prove-one.sh`   | Prove one input, check the proof with `cargo-zisk`, and write the wrapped proof as hex.         |

`ZISK_COORDINATOR_URL` selects the coordinator. Its default is the coordinator on the same machine. `network/up.sh` builds the guest and starts the prover as part of the local network; follow [Run it locally](/canton-zk-evm/run-it-locally) for that sequence.

The builder defaults to `prover/prove-one.sh` through `CZE_PROVE_CMD`. That command receives an input file and output folder and must leave `wrapped-proof.hex`. Its local `cargo-zisk` check does not enforce the chain's pinned programVK and rootC. The confirmers' sidecars enforce those pins. See the [builder settings](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/builder/README.md#settings).

## Reproduce the guest key

The build script uses one fixed build folder and refuses an ELF that still contains a build path. Run one guest build at a time because each build empties that shared folder.

On 2026-10-04, two fresh checkouts built sequentially by one user on one machine produced the same ELF and programVK. The builds took 133 s and 126 s. To reproduce the build as another user or on another machine, check that the resulting programVK matches the recorded value. `network/up.sh` stops if the build's pins differ. The [guest README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/guest/README.md#building) describes the build and its limits.

## Proof output

The wire format is 1,344 bytes: 768 bytes of PLONK proof, 32 bytes of programVK, 32 bytes of rootC and 512 bytes of public values. `prove-one.sh` writes the hex form and the JSON produced by `cargo-zisk-dev export-solidity-calldata`.


# Explorer

Read Ostia's EVM blocks beside their Canton records and check a proof in the browser.

The explorer combines reth's EVM data with Canton block records that it reads using the reader party's identity. It has block, transaction and address pages, a tROME faucet and a Verify button for checking a block in the browser.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [explorer README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/explorer/README.md).

## Read a block

The block page shows its EVM data beside its Canton update id, record time and the operator and confirmer that signed its record. It compares reth's block hash, the record's hash and the hash of the recorded header. It also displays the program key, ZisK release and block hash read from the proof beside the explorer's expected values.

A block being proven is not yet on Canton. A transaction may be waiting, being proven or final. The explorer does not show the block's Daml legs: `BlockRecord` does not carry them.

The explorer keeps its index in memory. It rebuilds that index from Canton's records at startup. The supplied network also keeps those records in memory and loses them on restart.

## Check the proof in your browser

Reading values from the proof does not verify it. Pressing Verify downloads a WebAssembly build of the sidecar's `verify` function and runs it on the recorded proof, header and transactions.

The check verifies the PLONK proof under the pinned program key and ZisK release. It checks that the header hashes to the proven block hash and that the transactions match the header's transaction root. The panel also requires that block hash to match reth's hash for the block.

The expected pins come from the explorer's status response. Verify runs the sidecar's block check; it does not run the separate `legs` check. The button is disabled while the block is being proven. After a check, the panel shows its result, duration and the module's download size. The duration excludes the download. See the [browser verifier README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/explorer/verify/README.md).

## Use the tROME faucet

The default payout is 1 tROME. Each address can request a payout once per 24 hours, and the faucet sends payouts at least 2 seconds apart. These limits are held in memory and reset when the explorer restarts. tROME has no value.

The faucet sends a transaction to reth. It arrives in the next proven block, so the builder must run for a payout to become final. Neither starting the network nor starting the explorer runs the builder.

The faucet's funded genesis must be prepared before the network starts. `explorer/run.sh setup` creates that funding and a fresh key for the run. `explorer/run.sh start` starts the container, and `explorer/run.sh stop` stops it. With no faucet key, the faucet is off. The full explorer guide has the startup sequence; the [demo guide](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md#running-it) has the sequence that includes both the demo and faucet funding.

## Local access

The container runs beside the chain on the same Linux machine with Docker and listens locally. The network has no Ledger API authentication. The explorer's code restricts its reads to the reader party, but anyone who can reach that participant can also read records for its other parties. The explorer README says not to give it a public address under these conditions.


# Measurements

Recorded proof and settlement measurements with their dates, hardware and limits.

The prover benchmark and the runs on Ostia measured different guests and workloads. The benchmark used the unmodified upstream reth guest. The Ostia runs used this repository's guest with its chain-rules check and the gateway.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [metrics report](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/docs/METRICS.md) and [demo results](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/README.md#results).

## Hardware

Both sets used one NVIDIA RTX PRO 6000 Blackwell Server Edition GPU. Only the GPU and driver were recorded for the Ostia demo.

| Item                      | Prover benchmark, 2026-10-03                                               | Ostia runs, 2026-10-05 |
| ------------------------- | -------------------------------------------------------------------------- | ---------------------- |
| CPU                       | AMD EPYC 9B45, 48 vCPUs; one socket, two threads per core                  | Not recorded           |
| Host RAM                  | 176 GiB as reported by the host                                            | Not recorded           |
| GPU memory                | 97,887 MiB, described as 96 GB; PCIe gen 5 x16                             | Capacity not recorded  |
| Driver                    | 580.178.04                                                                 | 580.178.04             |
| CUDA                      | 13.0 at the driver level; prebuilt GPU binaries, no CUDA toolkit installed | Not recorded           |
| Operating system and disk | Ubuntu 22.04.5, 500 GB boot disk                                           | Not recorded           |
| Node and snarkjs          | 20.18.0 and 0.7.6                                                          | Not recorded           |

## Pinned versions

These are the repository's versions, as listed in the metrics report and [PINS](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/PINS). The benchmark host's Node version above differs from the prover's pin.

| Component                      | Version                                                          |
| ------------------------------ | ---------------------------------------------------------------- |
| ZisK                           | 1.3.1-alpha                                                      |
| Upstream guest source          | 0.13.1                                                           |
| reth                           | 2.5.2                                                            |
| Canton                         | 3.6.0-snapshot.20260930.20337.0.ve610bc8f                        |
| Daml SDK for the external call | 3.5.13-snapshot.20261001.145.0.v852bdd82                         |
| Daml compiler                  | 3.6.0-snapshot.20260915.14804.0.v53478765, targeting Daml-LF 2.4 |
| Daml SDK for Script tests      | 3.5.12, targeting Daml-LF 2.1                                    |
| Java                           | 21                                                               |
| Rust                           | 1.94.1                                                           |
| Builder and demo Python        | 3.12 or newer                                                    |
| Prover Node and snarkjs        | 20.20.2 and 0.7.6                                                |
| Explorer Node                  | 22.18 or newer locally; 24 in the container and CI               |
| Test-token Solidity compiler   | 0.8.28                                                           |

## Prover benchmark: 2026-10-03

The table gives median times in seconds from three warm runs for each block size. Each block contained plain transfers. Every proof passed `cargo-zisk verify`. The raw benchmark logs are not in the repository.

| Block                     | Proof without wrap, fresh process | Wrap alone, fresh process | Proof with wrap, fresh process | Whole fresh process | Running prover: input to wrapped proof | Running prover: input to verified proof |
| ------------------------- | --------------------------------- | ------------------------- | ------------------------------ | ------------------- | -------------------------------------- | --------------------------------------- |
| 1 transfer, 21 kgas       | 4.6                               | 2.4                       | 7.4                            | 21.1                | 6.7                                    | 7.0                                     |
| 1,000 transfers, 21 Mgas  | 6.9                               | 2.4                       | 9.6                            | 24.5                | 9.0                                    | 9.3                                     |
| 5,000 transfers, 105 Mgas | 17.9                              | 2.4                       | 20.8                           | 34.9                | 21.2                                   | 21.5                                    |

The running prover kept the coordinator, worker and keys loaded. The fresh-process totals include starting and stopping the prover. Input-to-proof times start with an existing witness file; they do not measure building the witness. The 105 Mgas case was a benchmark of the upstream guest, not an Ostia block. Ostia's pinned block gas cap is 30,000,000.

| Proof measurement        | Wrapped PLONK        | Unwrapped STARK          |
| ------------------------ | -------------------- | ------------------------ |
| Proof file size          | 2,942 to 2,944 bytes | 415,232 to 415,234 bytes |
| `cargo-zisk verify` time | 0.29 to 0.30 s       | 0.03 s                   |
| Wire size                | 1,344 bytes          | Not measured             |

Verifying the wrapped proof includes starting Node to run snarkjs. It is not a measurement of the sidecar's native verifier. The 1,344-byte wire form contains 768 bytes of proof, 32 bytes of program key, 32 bytes of recursion root and 512 bytes of public values.

## Startup and memory

These measurements are from the same 2026-10-03 benchmark with ZisK 1.3.1-alpha.

| Measurement                                                                                                      | Result                                                   |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| First 1-transfer run after deleting ZisK's cache and dropping the page cache, with the proving key still on disk | 123 s wall time; proof work took 14.5 s                  |
| Running prover startup with the guest's compiled cache already present                                           | Keys loaded in 7.0 s, then 9.5 s to register the program |
| First proof request after that startup                                                                           | 6.56 s                                                   |
| Peak GPU memory reported by `nvidia-smi`                                                                         | 93.2 GiB                                                 |
| Host RAM rise during a fresh-process proof                                                                       | 27 to 29 GiB, plus 22.9 GiB of shared memory             |
| Host RAM rise while the running prover served requests                                                           | 3 GiB, plus 22.9 GiB of shared memory                    |
| GPU memory reported for the PLONK wrap alone                                                                     | 30,054,767,337 bytes, about 28.0 GiB                     |

The prover sizes a large GPU buffer to the card. Peak use was about the same for the smallest and largest benchmark blocks. It does not establish the minimum GPU memory needed for the whole prover.

## Ostia demo: 2026-10-05

Each case ran once on the local Canton network, with the prover already running, from commit `6b445073163832e9023850f53678617d90dfc808`. These are individual test results. The [results folder](https://github.com/rome-protocol/canton-zk-evm/tree/a84e84e439a5cc669c0ae61facf7c8c995d9d564/demo/results) holds the records. [Overview](/canton-zk-evm/canton-zk-evm#what-the-demo-showed) describes the five outcomes.

| Case                  | Block and gas used                                                               | Proof                                                           | Finished proof file to builder exit | Whole builder run                |
| --------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------- | -------------------------------- |
| Smoke                 | Block 1, empty; 0 gas                                                            | 6.31 s                                                          | 0.49 s                              | 155.20 s                         |
| Setup                 | Block 2, two transactions; gas not recorded                                      | 6.71 s                                                          | Not recorded                        | 8.00 s                           |
| Deposit               | Block 3, one transaction; 122,308 gas                                            | 6.72 s                                                          | 0.66 s                              | 8.14 s                           |
| Payment               | Block 4, three transactions; 185,307 gas                                         | 6.72 s                                                          | 0.42 s                              | 7.87 s                           |
| Withdrawal            | Block 5, one transaction; 90,345 gas                                             | 6.72 s                                                          | 0.58 s                              | 8.02 s                           |
| Allocation taken back | Block 6 refused, then rebuilt with one transaction and no legs; gas not recorded | 6.75 s for the refused build; second proof not timed separately | Refused submission not timed        | 8.09 s refused; 8.04 s committed |
| Tampered proof        | Block 7 refused; gas not recorded                                                | 6.74 s before tampering                                         | Refused submission not timed        | 7.57 s                           |

Each proof was 1,344 bytes. The interval from the finished proof file to builder exit includes fetching the gateway's account and storage proofs, `Advance`, commit on both confirming participants and reth marking the block final. It is longer than Canton's commit alone. Matching legs to Canton contracts happens before proving and is outside that interval.

The smoke test's whole run includes witness and input preparation and the prover's initial setup of the guest program. It may also include building the input tool. Separate times for those steps were not recorded.

## Other recorded checks

On 2026-10-04, the first transfer proof covered block 1 on a temporary copy of the genesis, not a running chain: one 21,000-gas transfer and 385,277 guest steps. Three proofs took 6.33, 6.75 and 6.69 s. ZisK's verifier and the repository's verifier crate checked the proof. Changing the input's chain id to 1 made the guest refuse it without a proof.

Building the guest from an empty build folder took 125 s, including dependency download. Two fresh checkouts took 133 s and 126 s and produced the same ELF and programVK as each other. The prover's first program setup took 43 s. These figures come from the 2026-10-04 records described in the metrics report and [guest README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/guest/README.md).

After the 2026-10-05 demo, the explorer's browser check of block 3 passed in 13 ms. About 0.35 s elapsed from the click to the answer. This was one browser check, not a benchmark, and is not recorded in `demo/results/`.

The WebAssembly check took 8 to 32 ms in Node on CI runners. That test verified a proof and then rejected a mismatched header. It was neither a successful full-block check nor a browser measurement; see the [verifier README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/explorer/verify/README.md).

## What these measurements do not establish

There is no systematic benchmark of contract calls or precompiles, blocks above 105 Mgas, or the benchmark's block sizes with Ostia's guest. There are no measurements with multiple GPUs or multiple blocks in flight.

Witness construction on a warm host, native sidecar verification and Canton's commit of a large block have not been timed separately. The demo committed blocks with at most three transactions and one leg. Each case ran once, so the results do not show how much timings vary between runs.


# Trust model and limits

The checks, authorities, custody assumptions and limits of the Ostia early release.

Ostia is an early release on a local test network. It has one builder, one operator and one confirmer. The code and ZisK 1.3.1-alpha have not been audited. tROME is a test coin with no value. Do not use this release for anything of value.

This page describes commit `a84e84e439a5cc669c0ae61facf7c8c995d9d564`. Read the full [design](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/docs/DESIGN.md) and [security policy](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/SECURITY.md).

## Who checks what

| Part             | Check or authority                                                                                                                                                   |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Guest            | Validates the EVM block and rejects a chain configuration other than Ostia's compiled rules.                                                                         |
| Sidecar `verify` | Checks the proof, pinned programVK and rootC, proven block hash, header and transaction list.                                                                        |
| Sidecar `legs`   | Checks the gateway's account and storage proofs and compares the ordered attached legs with the proven gateway hash.                                                 |
| Daml `Advance`   | Checks the sidecar's answers, parent, next block number, gas cap and the contracts used by the legs. It settles the legs and creates the next head and block record. |
| Canton           | Orders blocks through the consumption of the chain's one state contract and commits the transaction's changes together. It rejects differing external-call answers.  |
| Builder          | Selects transactions, checks the Canton contracts it can see, obtains a proof and submits the block. It cannot finalize a block itself.                              |

One sidecar runs beside each confirming participant, including the operator's participant. Each sidecar computes its answers solely from the input sent in the external call. Daml compares their answers; it does not perform the cryptographic proof check itself. The [sidecar README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/sidecar/README.md) and [Daml README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/daml/README.md) give the checks and authority requirements.

## What the proof fixes

The pinned program key identifies the guest and its chain rules. The proof commits to a block hash. Checking its header binds the state root and transaction root. Account and storage proofs then bind the gateway's recorded leg hash to that state root.

The attached legs must have the same fields and order as those the gateway recorded. Removing a leg, adding one or changing a recipient, amount, token or id fails the hash comparison. A block with no legs must prove that absence too. The leg comparison also relies on Keccak-256's collision resistance.

The chain record's genesis state root fixes the gateway code. The gateway contract has no owner, admin key, upgrade or pause. The Canton party still has custody authority over the tokens it holds.

## Operator, confirmer and gateway authority

The operator and gateway propose the chain's genesis, pins, gas cap and builder. The confirmer accepts the proposal. It is expected to compare the pins and genesis gateway code with its own build. The supplied local scripts propose matching values and immediately accept the proposal on the confirmer's behalf.

The operator, confirmer and gateway sign `ZkChain`. They are the chain's root of trust. Deposit and withdrawal settlement choices require all three. Payment settlement requires the operator and confirmer. `Advance` provides their joint authority under the Daml design. The builder alone cannot exercise the settlement choices.

The gateway party holds the Canton tokens backing wrapped tokens on the EVM. It is hosted on the operator's participant. Whoever runs that participant can move the locked tokens outside the withdrawal rules. The gateway contract's lack of an admin key does not constrain that Canton party's ability to act through its participant.

Token registration is another trust decision. The operator and gateway propose the mapping from a Canton instrument to an EVM wrapped token, and the confirmer accepts it. The gateway executes the registered token's Daml transfer code with its own authority. The [network README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/network/README.md#the-canton-network) describes the participant hosting and registration checks.

## What can stop a block

To include a leg, the builder needs read access to its requests, terms, acceptances, allocations, token registrations and gateway holdings. It leaves out a transaction whose Canton contracts it cannot see.

An allocation can be taken back after the builder checks it or after proving. A token registry can also refuse settlement under its own rules. Canton then refuses the block and nothing commits. For a registry refusal the builder could not predict, the next run needs `--exclude`; the builder does not remember that refusal across runs.

Each builder run handles one block; it does not loop over successive blocks. The builder does not cap execution steps before proving. If the prover cannot finish a block, the builder run stops. The [builder README](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/builder/README.md#not-here) records these limits.

## What this release does not provide

The local network keeps no state between runs. It has no Ledger API authentication or TLS and is intended for one machine. The network uses pinned Canton 3.6 snapshots and a snapshot Daml compiler for the external call. Their versions and hashes are in [PINS](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/PINS).

Multiple builders, forced inclusion, public Canton deployment, privacy and custody requiring more than the operator's participant are outside this release. There are no throughput or block-time targets. The recorded demo used the same sidecar implementation on both confirming participants, on the same machine as Canton.

The reader can re-check block proofs, but `BlockRecord` does not carry the settled legs. The browser's Verify button checks the proof, header and transactions; it does not repeat the Canton leg settlement checks.

## Report a security problem

Use GitHub's private vulnerability reporting for `canton-zk-evm`: open the repository's Security tab and choose **Report a vulnerability**. Do not open a public issue for a security problem.

Include the affected component, commit, steps to reproduce and observed result. Attach the proof or block if it is part of the finding. The [security policy](https://github.com/rome-protocol/canton-zk-evm/blob/a84e84e439a5cc669c0ae61facf7c8c995d9d564/SECURITY.md) defines the scope. There is no bug bounty.


# Deploy Solidity Contracts

Deploy Solidity smart contracts on Solana with Hardhat or Foundry — Rome network configuration, funded accounts, and step-by-step deployment.

This guide covers deploying Solidity smart contracts on Rome EVM using Hardhat and Foundry.

## Prerequisites

* Node.js v22.13+ (Hardhat) or Foundry installed
* A funded Rome EVM address (see [Quickstart](/getting-started/quickstart))
* Your private key exported as an environment variable

```bash
export PRIVATE_KEY="0xYOUR_PRIVATE_KEY"
```

## Network Configuration

| Network               | RPC URL                                     | Chain ID | Explorer                               |
| --------------------- | ------------------------------------------- | -------- | -------------------------------------- |
| Hadrian (testnet)     | `https://hadrian.testnet.romeprotocol.xyz/` | `200010` | `via-hadrian.testnet.romeprotocol.xyz` |
| **Rubicon (mainnet)** | `https://rubicon.mainnet.romeprotocol.xyz/` | `7531`   | `via-rubicon.mainnet.romeprotocol.xyz` |

Gas is USDC on every network. On Hadrian you get it by bridging test USDC or wrapping from a Solana devnet wallet ([Faucets](/resources/faucets)); on Rubicon by bridging USDC from Ethereum, Base, Arbitrum, Polygon, Avalanche, Monad or OP, or from a Solana wallet, with the [Rome Bridge](https://bridge-api.mainnet.romeprotocol.xyz/). Deploy to Rubicon with the same artifacts you tested on Hadrian — only the chain id and the wrapper addresses differ.

## Hardhat

### Setup

```bash
mkdir my-rome-project && cd my-rome-project
npx hardhat --init
```

Accept the defaults (Hardhat 3, current directory, TypeScript + viem template); dependencies install automatically.

### hardhat.config.ts

```typescript
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";

export default defineConfig({
  plugins: [hardhatToolboxViemPlugin],
  solidity: {
    profiles: {
      default: { version: "0.8.28" },
      production: {
        version: "0.8.28",
        settings: { optimizer: { enabled: true, runs: 200 } },
      },
    },
  },
  networks: {
    hadrian: {
      type: "http",
      chainType: "l1",
      chainId: 200010,
      url: "https://hadrian.testnet.romeprotocol.xyz/",
      accounts: [configVariable("PRIVATE_KEY")],
    },
    rubicon: {
      type: "http",
      chainType: "l1",
      chainId: 7531,
      url: "https://rubicon.mainnet.romeprotocol.xyz/",
      accounts: [configVariable("PRIVATE_KEY")],
    },
  },
});
```

`configVariable("PRIVATE_KEY")` resolves from the environment (or the encrypted keystore — `npx hardhat keystore set PRIVATE_KEY`).

### Deploy

```bash
npx hardhat run scripts/deploy.ts --network hadrian
```

### Verify on Block Explorer

```bash
npx hardhat verify --network hadrian 0xCONTRACT_ADDRESS
```

Needs the Sourcify verifier config from [Verify Contracts](/developer-guides/verify-contracts) in your `hardhat.config.ts`.

## Foundry

### Setup

```bash
forge init my-rome-project
cd my-rome-project
```

### Deploy

```bash
# Testnet
forge create --rpc-url https://hadrian.testnet.romeprotocol.xyz/ \
  --private-key $PRIVATE_KEY \
  src/Counter.sol:Counter
```

### Call Deployed Contract

```bash
# Read
cast call 0xCONTRACT_ADDRESS "number()" \
  --rpc-url https://hadrian.testnet.romeprotocol.xyz/

# Write
cast send 0xCONTRACT_ADDRESS "increment()" \
  --rpc-url https://hadrian.testnet.romeprotocol.xyz/ \
  --private-key $PRIVATE_KEY
```

## Using the Rome Solidity SDK

For contracts that interact with Solana programs, use the interfaces from the public [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo. It is installed from GitHub, not from npm:

```bash
# Hardhat / npm
npm install github:rome-protocol/rome-solidity

# Foundry
forge install rome-protocol/rome-solidity
# then in foundry.toml, under [profile.default]:
# remappings = ["@rome-protocol/rome-solidity/=lib/rome-solidity/"]
```

Contracts beyond `interface.sol` and the oracle interfaces (the ERC-20 wrappers and factory, for example) import OpenZeppelin. With npm it comes along as a dependency. With Foundry, also run `forge install OpenZeppelin/openzeppelin-contracts` and add the remapping `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/`.

With npm, the package installs as `@rome-protocol/rome-solidity`, so the import below resolves from `node_modules`. With Foundry, the remapping makes the same import resolve from `lib/rome-solidity`.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ICrossProgramInvocation, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

contract MyRomeContract {
    IHelperProgram constant Helper =
        IHelperProgram(0xFF00000000000000000000000000000000000009);
    ICrossProgramInvocation constant Cpi =
        ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);

    // Transfer an SPL token from the caller's PDA
    function moveSpl(address to, uint64 tokens, bytes32 mint) external {
        Helper.transfer_spl(to, tokens, mint);
    }

    // Invoke any Solana program via CPI
    function callSolanaProgram(
        bytes32 programId,
        ICrossProgramInvocation.AccountMeta[] calldata accounts,
        bytes calldata data
    ) external {
        Cpi.invoke(programId, accounts, data);
    }
}
```

## Bring an existing protocol

Any Solidity contract deploys on Rome unmodified — a compliance contract, a DeFi protocol, or an existing hardened application. The one thing that is not in your contract is the **one-time wrapper grant**: a user who moves SPL-wrapper tokens (wUSDC and the like) through your contract has to grant that token's wrapper the right to move their tokens once, before the first `transferFrom`. Without it the transfer reverts with SPL error `0x4`. Check `isEnabled(user)` on the wrapper and see [One-time grant per wallet](/core-concepts/token-interop#one-time-grant-per-wallet); your front end sends the grant, your contract does not change. Two production examples you can fork and deploy with the same Hardhat / Foundry flow above:

* [**compound-on-rome-comet**](https://github.com/rome-protocol/compound-on-rome-comet) — canonical Compound v3 (Comet), standard Foundry.
* [**rome-aave-v3**](https://github.com/rome-protocol/rome-aave-v3) — canonical Aave v3.

Both gain Solana execution and CPI composability with no contract changes; their users make the one-time wrapper grant described above before the first deposit.

## Gas

Let your tooling estimate gas (`eth_estimateGas`; Hardhat, `forge create` and `cast` all do) and never hardcode `21000`. On Rome most of a write's cost is creating Solana accounts, such as a deployed contract, a new token account or a never-used recipient, and the estimate includes them. For a call that makes Solana CPIs the estimate can come out low, so keep a margin. Rome charges the gas actually used. Measured on Hadrian, deploying a 10-byte contract estimated and used 1,147,921 gas, and a native transfer to an existing address estimated 21,000 and used 10,001. A real contract costs more, so estimate your own deploy. The [JSON-RPC page](/reference/json-rpc#gas-is-solana-derived-estimate-dont-hardcode) has the measured table.

## Deployment Constraints

| Constraint             | Limit              | Notes                                                        |
| ---------------------- | ------------------ | ------------------------------------------------------------ |
| Max contract size      | 24 KB              | Same as Ethereum (EIP-170, 24,576 bytes)                     |
| Transaction size limit | 80 KB per holder   | Large deploys are split across holder accounts transparently |
| Compute budget         | \~1.4M CU (atomic) | Use iterative mode for heavy contracts                       |
| Solidity version       | 0.8.28 recommended | Earlier versions work but 0.8.28 matches the SDK             |

## Common Errors

| Error                                                                | Cause                                                                              | Fix                                                                                                                                                                                                                                                 |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `insufficient funds for gas`                                         | EVM address has no gas token                                                       | Hadrian: bridge test USDC in with the [Rome Bridge](/apps-on-rome/bridge-api) (see [Quickstart](/getting-started/quickstart), step 2); Rubicon: bridge real USDC in with the [Rome Bridge on mainnet](https://bridge-api.mainnet.romeprotocol.xyz/) |
| `nonce too low`                                                      | Stale nonce in wallet                                                              | Reset MetaMask account or specify nonce manually                                                                                                                                                                                                    |
| `execution reverted`                                                 | Contract logic failed                                                              | Debug with `eth_call` or `forge test --fork-url`                                                                                                                                                                                                    |
| `execution reverted` with SPL error `0x4` when moving wrapper tokens | The user has not made the one-time wrapper grant                                   | Check `isEnabled(user)` on the wrapper and send `approve_spl` once ([details](/core-concepts/token-interop#one-time-grant-per-wallet))                                                                                                              |
| `transaction underpriced`                                            | Gas price below minimum                                                            | Increase gas price in transaction                                                                                                                                                                                                                   |
| A transfer or deploy fails with a fixed gas limit                    | A hardcoded limit such as `21000` is below what creating the Solana accounts costs | Remove it and use `eth_estimateGas`                                                                                                                                                                                                                 |

## What's Next

* [Open your contract to Solana users](/developer-guides/open-to-solana-users) — let Solana wallet users call what you just deployed
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — use CPI precompiles to interact with Solana programs
* [Verify Contracts](/developer-guides/verify-contracts) — publish your source to the block explorer


# Open your contract to Solana users

Deploy a Solidity contract on Rome (Hadrian testnet, Rubicon mainnet) and open it to Solana wallets — what the contract needs, how a Solana wallet user calls it, per-network addresses, and a launch ch

One deployment, two kinds of users. A contract on Rome is reached by MetaMask users with their EVM key and by Solana wallet users (Solflare, for example) with their Solana key — same address, same state. This page is the end-to-end path: deploy, make the contract Solana-ready, wire the Solana lane in your app, and ship on mainnet.

## 1. Deploy on the network you want

Rome chains are standard EVM targets: Hardhat, Foundry, viem and ethers work unchanged (full walkthrough in [Deploy Solidity Contracts](/developer-guides/deploy-solidity)).

| Network                                    | Chain ID | RPC                                         | Explorer                                                                              | Gas token | How to get gas                                                                                                                                                                         |
| ------------------------------------------ | -------- | ------------------------------------------- | ------------------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [**Rubicon**](/networks/rubicon) (mainnet) | `7531`   | `https://rubicon.mainnet.romeprotocol.xyz/` | [via-rubicon.mainnet.romeprotocol.xyz](https://via-rubicon.mainnet.romeprotocol.xyz/) | USDC      | Bridge USDC from Ethereum, Base, Arbitrum, Polygon, Avalanche, Monad or OP (CCTP) or from Solana with the [Rome Bridge](https://bridge-api.mainnet.romeprotocol.xyz/); it lands as gas |
| **Hadrian** (testnet)                      | `200010` | `https://hadrian.testnet.romeprotocol.xyz/` | [via-hadrian.testnet.romeprotocol.xyz](https://via-hadrian.testnet.romeprotocol.xyz/) | test USDC | Bridge test USDC from a testnet ([Faucets](/resources/faucets)) or wrap from a Solana devnet wallet in the [Rome App](https://app.devnet.romeprotocol.xyz)                             |

Gas is USDC with 18 decimals on the EVM side; there is no Rome faucet on any network. Writes cost more gas than Ethereum intuition suggests, because most of the cost is creating Solana accounts: a never-used recipient, a new token account, a deployed contract. Use `eth_estimateGas`, which standard tooling does, and never hardcode 21,000. Measured on Hadrian, deploying a 10-byte contract estimated and used 1,147,921 gas, and a native transfer to a never-used address 1,086,961, against 21,000 for a transfer to an existing address. Rome charges the gas actually used, which in these measurements was never more than the estimate. Fund a few USDC before a first deploy. Verify source on the explorer afterwards ([Verify Contracts](/developer-guides/verify-contracts); the mainnet verifier is `https://verify.mainnet.romeprotocol.xyz`).

Develop on Hadrian, then redeploy the same artifacts to Rubicon with the chain id and the mainnet wrapper addresses swapped; nothing in the contract changes between networks. Rubicon is not in the public registry, `create-rome-app` or `rome-cli` yet, so tools that read chain values from them cannot target it; fill the values by hand from [Rubicon](/networks/rubicon). Gas on Rubicon is real USDC at about ten times Hadrian's gas price, so a deploy costs real money there.

## 2. What makes a contract Solana-ready

A Solana user's EVM identity is their **synthetic address**, `keccak256(solana_pubkey)[12:]`: an ordinary `msg.sender`, with an ordinary nonce. Most contracts need no change at all. The three things that matter:

* **Take value as ERC-20, not `msg.value`.** A Solana user's funds are SPL tokens in their wallet, seen on the EVM side as the token's wrapper (`wUSDC`, `wETH`, `wSOL`). The synthetic holds nothing at rest and the user's assets arrive as the wrapper, so pull with `transferFrom` after an `approve` and return with `transfer`. Each user grants the wrapper its SPL delegate right once before the first pull ([One-time: grant the wrapper](/developer-guides/dual-lane-app#one-time-grant-the-wrapper)). A `payable` entry point only works once the synthetic has been given native gas (the relay path's `rome_composeExternalDepositV1`), which the fund-and-sweep model does not do. An existing ERC-20 vault, AMM or lending market already has the right shape.
* **Use the canonical wrappers.** Each chain has one wrapper per mint; the addresses are in [Contract Addresses](/reference/contract-addresses) (Hadrian's also in the public [registry](https://github.com/rome-protocol/rome-registry)). Point constructors at those addresses; do not deploy your own.
* **Fit the atomic budget where a wallet signs.** A Solana wallet signs one Solana transaction, so a Solana-lane call must fit it: about 1.4M compute units, 64 accounts, 4 KB ([Constraints](/core-concepts/constraints)). Measured on Hadrian: a transfer, an approve and an Aave supply fit; a Uniswap V3 swap fits with little headroom (about 1.24M CU); an Aave borrow does not and takes the relay path in the next section. Measure with the Solana receipt's `computeUnitsConsumed`, requesting the transaction with `maxSupportedTransactionVersion: 1` (Rome legs are v1 transactions; without it the field reads 0).

What you do not need: a Solana program, a bridge, a second deployment, or any Rome-specific Solidity. If the contract also calls Solana programs through the precompiles, see [Call Solana from EVM](/developer-guides/call-solana-from-evm).

## 3. Let a Solana wallet call it

Your app's EVM path is unchanged (MetaMask + `submitRomeTx`, or plain viem/ethers). For the Solana path there are two shapes.

**The SDK lane — one Solana v1 transaction per call.** [`@rome-protocol/sdk`](https://github.com/rome-protocol/rome-sdk-ts) (`submitRomeTxV1`) has the proxy compose one Solana v1 transaction for your EVM call (up to 64 accounts and 4 KB, with no address lookup table; there is no ComputeBudget instruction, treasure wallet or account discovery in your code). The wallet signs twice with the same key: first a readable authorization message (`Rome authorization` on one line, `0x` and the EVM transaction hash on the next), which the chain checks through the Ed25519 program, then the transaction. The app sends the signed transaction to the Solana RPC; the call returns the EVM transaction hash and its receipt. Before the wallet signs the transaction, the SDK checks it: the payer is the only signer, only the Rome program and the Ed25519 verifier run, the Rome instruction carries exactly the call the user authorized, and there is no priority fee. Anything else is refused.

If a call does not fit one transaction, the composer refuses it and the SDK hands it to the proxy to relay, with the same authorization signature, so the user still sees one authorization prompt. The result then has `route: "relay"`. The rules for relayed calls and long calls are in [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls). A relayed call is paid in the chain's native gas, so the synthetic needs a gas balance: `fundSyntheticGas(deps, amountWei)` moves the chain's gas token (USDC on Hadrian and Rubicon) from the wallet's token account into it, in one wallet-signed v1 transaction.

The fund leg (`buildFundLeg`) and the wallet token-account creation before a sweep are plain Solana instructions; `submitSolanaInstructionsV1` sends them as v1 transactions too, signed by the same wallet signer, with no proxy involved. The sweep's own call (`buildSweepLeg`, `HelperProgram.transfer_spl`) goes through `submitRomeTxV1` like any other, and the proxy finds the token accounts. `submitRomeTxV1` does not create a new synthetic's account by itself: before the first value-moving call, check `isSyntheticProvisioned(...)` and, if it is false, submit `buildCreatePdaCall(synthetic)` through `submitRomeTxV1` once. A full deposit-and-withdraw walkthrough with code is in [Build a dual-lane app](/developer-guides/dual-lane-app); the mechanics are in [Call EVM from Solana](/developer-guides/call-evm-from-solana). The earlier `submitRomeTxSolanaLane`, which builds a legacy Solana transaction in the client, is still in the SDK as the older path.

**Where each `deps` value comes from.** The SDK's `deps` object has five required fields (the rest, such as `fetchImpl` and the timeouts, are optional). Four come from the chain and one from the wallet:

| `deps` field | Where it comes from                                                                                                  | Registry getter (`@rome-protocol/registry` v0.2.0)                                                   |
| ------------ | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `proxyUrl`   | the chain's RPC URL                                                                                                  | `getChain(chainId).rpcUrl`                                                                           |
| `chainId`    | the chain ID                                                                                                         | `getChain(chainId).chainId`                                                                          |
| `programId`  | the Rome program ID the chain runs on                                                                                | `getChain(chainId).romeEvmProgramId`                                                                 |
| `connection` | a Solana RPC for the chain's cluster, where the signed transaction is sent (devnet for Hadrian, mainnet for Rubicon) | `getChain(chainId).solana.rpc` (a public devnet URL for Hadrian); for mainnet use an RPC of your own |
| `signer`     | the wallet's `LaneSigner`, or a `Keypair` in scripts                                                                 | none                                                                                                 |

The leg builders take values that are not in `deps`. Their `mint` (called `usdcMint` in the walkthrough) is the gas token's Solana mint, from `getTokens(chainId)` (the entry with `kind: "gas"`, field `mintId`). The wUSDC wrapper address is not a leg-builder input; you use it for the ERC-20 calls, and it is the entry with `kind: "spl_wrapper"` and symbol `wUSDC` (field `address`). The registry covers Hadrian. For Rubicon, fill them by hand: `proxyUrl` `https://rubicon.mainnet.romeprotocol.xyz/`, `chainId` `7531`, `programId` `RomePq9X3iAoTr813HR7uRarafFDUJty6GdiEceYpzX`, `mint` `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`, the wrapper from the table in the next section, and a Solana mainnet RPC for `connection`. [Rubicon](/networks/rubicon) has the full list.

**The proxy's external-V1 endpoints — the same lane without the SDK.** The SDK wraps the atomic compose, the relay and the gas deposit (`fundSyntheticGas`). The raw RPC methods remain for clients that do not use the SDK, and they include the iterative compose, which the SDK does not wrap. The wallet signs one Ed25519 message over the call (the readable form `Rome authorization\n0x<hash>` or the raw hash), and the proxy either composes the Solana transaction(s) for the wallet to sign (`rome_composeUnsignedAtomicV1`, `rome_composeUnsignedIterativeV1` + `rome_submitSignedV1Plan`) or runs the whole execution itself with one wallet signature (`rome_relayUnsignedV1`, after a `rome_composeExternalDepositV1` (the SDK's `fundSyntheticGas`) that funds the synthetic's gas balance; relayed calls spend from it, so top it up when it runs low). Method shapes in [JSON-RPC Support](/reference/json-rpc); the authorisation rules in [Call EVM from Solana](/developer-guides/call-evm-from-solana#the-external-v1-endpoints). Use the relay for anything iterative, and the SDK lane for everything that fits.

Either way the contract sees a normal transaction from the synthetic address and the user never holds an EVM key.

## 4. Per-network wrapper addresses

| Network          | `wUSDC` wrapper                              | Other wrappers                                                                                           |
| ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Rubicon (7531)   | `0x0952d233b0c254cd375f3bfb3e5869c1a906f9b3` | `wETH` `0x01e8b3573da1d4b7b4b1ce0932cf07c8af0c92e9`, `wSOL` `0xc4e29e0f726a906e93277cbbc37bf27a9848a113` |
| Hadrian (200010) | `0xbce339ba4c3a8de3bb8af1c5539e6ac11da3112d` | `wETH` `0x02aeace038269f9961c93d04dcacd6fb3adfa55f`, `wSOL` `0x39f36444fcb79b3e691544aee2a832ebef54ca9f` |

The native gas token appears as `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` in token lists; it is not an ERC-20 contract. Wrappers carry the SPL mint's decimals, not 18: wUSDC 6, wETH 8, wSOL 9.

## 5. Launch checklist

1. Deployed on Hadrian; the EVM path works from MetaMask.
2. Every value-moving entry point is ERC-20 based and pointed at the chain's canonical wrapper.
3. A Solana wallet that signs v1 transactions (Solflare does on devnet today; Phantom does not yet) completes the full round trip on Hadrian: provision the synthetic (`create_pda`, once) → fund leg → grant → approve → call → sweep leg, using the SDK lane; or the relay path if the call is iterative.
4. Each Solana-lane call's `computeUnitsConsumed` is read from the Solana receipt and sits under the atomic budget, or the call is routed through the relay.
5. The wrapper grant is in your app's first-deposit flow for both lanes: `isEnabled(user)` is checked and `approve_spl` is sent once when it is false ([One-time: grant the wrapper](/developer-guides/dual-lane-app#one-time-grant-the-wrapper)).
6. Source verified on the explorer.
7. Redeployed on Rubicon with the mainnet wrapper addresses. Rubicon is not in the registry, so its `deps` values are filled by hand from [Rubicon](/networks/rubicon). The same round trip is run once with real funds, using a wallet you have checked signs v1 transactions on mainnet.

## What's next

* [Build a dual-lane app](/developer-guides/dual-lane-app) — the vault example, step by step, both wallets
* [Call EVM from Solana](/developer-guides/call-evm-from-solana) — synthetic addresses, account discovery, the external-V1 endpoints
* [Deploy Solidity Contracts](/developer-guides/deploy-solidity) · [Verify Contracts](/developer-guides/verify-contracts) · [Contract Addresses](/reference/contract-addresses)


# Verify Contracts

Verify Solidity contract source on Rome via the hosted Sourcify service — standard forge verify-contract and hardhat-verify flows, badges on the Via explorer.

Rome runs a hosted [Sourcify](https://sourcify.dev) verification service. Once you verify a contract, its source and ABI become publicly reproducible and show up with a **✓ Source verified** badge on the Rome block explorer (Via).

Verification works with the standard `forge verify-contract` and `hardhat-verify` flows — you just point them at Rome's verifier.

## Verifier endpoint

| Environment       | Verifier URL                              |
| ----------------- | ----------------------------------------- |
| Testnet & Devnet  | `https://verify.testnet.romeprotocol.xyz` |
| Mainnet (Rubicon) | `https://verify.mainnet.romeprotocol.xyz` |

One shared instance serves every Rome testnet/devnet chain — you select the chain with the standard `--chain-id` flag.

## Foundry

After deploying (see [Deploy Solidity Contracts](/developer-guides/deploy-solidity)):

```bash
forge verify-contract <CONTRACT_ADDRESS> src/MyContract.sol:MyContract \
  --verifier sourcify \
  --verifier-url https://verify.testnet.romeprotocol.xyz/ \
  --chain-id <YOUR_CHAIN_ID> \
  --compiler-version 0.8.20
```

If the constructor took arguments, pass them so the bytecode matches:

```bash
  --constructor-args $(cast abi-encode "constructor(uint256)" 42)
```

You can also verify at deploy time with `forge create … --verify` or `forge script … --verify` (Foundry infers the chain id from `--rpc-url`).

## Hardhat

`hardhat-verify` ships with the Hardhat 3 toolbox. Point its Sourcify provider at Rome's verifier in `hardhat.config.ts` and run `hardhat verify`:

```typescript
export default defineConfig({
  // …plugins, solidity, networks…
  verify: {
    etherscan: { enabled: false },
    blockscout: { enabled: false },
    sourcify: {
      enabled: true,
      apiUrl: "https://verify.testnet.romeprotocol.xyz",   // Rubicon mainnet: "https://verify.mainnet.romeprotocol.xyz"
    },
  },
});
```

```bash
npx hardhat verify --network <your-network> <CONTRACT_ADDRESS> [constructor args…]
```

On success the verifier links the published source, e.g. `https://verify.testnet.romeprotocol.xyz/repo-ui/<CHAIN_ID>/<ADDRESS>`.

## Checking verification status

The explorer reads verification status from the verifier's API. You can query it directly:

```bash
curl https://verify.testnet.romeprotocol.xyz/v2/contract/<CHAIN_ID>/<ADDRESS>
```

A verified contract returns a `match` field:

* **`exact_match`** — bytecode *and* metadata hash match (a perfect, "full" match).
* **`match`** — bytecode matches; metadata differs slightly (still a genuine, verified match).
* `404` / `null` — not verified.

## Notes

* Verification needs only your contract's deployed bytecode + the source and compiler settings you submit — the same inputs as on any EVM chain.
* New Rome chains are onboarded to the verifier automatically; if you deploy to a fresh chain and verification can't find it yet, give it a short while or reach out.


# WebSocket Subscriptions

Subscribe to Rome EVM events over WebSocket — eth\_subscribe with newHeads and logs, working out of the box with ethers; with viem, use polling.

Rome EVM endpoints support Ethereum WebSocket subscriptions (`eth_subscribe` / `eth_unsubscribe`) over `wss://`. Connect to the same host as the HTTPS RPC, with the `wss://` scheme:

```
wss://<chain>.<network>.romeprotocol.xyz/
```

This is what the `ethers` `WebSocketProvider` uses under the hood, so `.on('block')` and `contract.on(event)` work out of the box. `viem` needs one setting, described in the [viem](#viem) section.

## Subscription types

| Type                     | Fires on                                  | Use for                                                             |
| ------------------------ | ----------------------------------------- | ------------------------------------------------------------------- |
| `newHeads`               | each new block                            | block tickers, refreshing balances/nonces, per-block bots, indexers |
| `logs`                   | matching events (by `address` / `topics`) | reacting to your contract's events, relayers/bridges, trading bots  |
| `newPendingTransactions` | —                                         | accepted but never emits (Rome has no public mempool)               |

## ethers (v6)

```javascript
import { ethers } from "ethers";

const provider = new ethers.WebSocketProvider(
  "wss://hadrian.testnet.romeprotocol.xyz/"
);

// newHeads
provider.on("block", (blockNumber) => {
  console.log("new block", blockNumber);
});

// logs — react to a contract event
const contract = new ethers.Contract(address, abi, provider);
contract.on("Transfer", (from, to, value) => {
  console.log("Transfer", from, to, value.toString());
});
```

## viem

`viem`'s `webSocket` transport registers a subscription only when the id is a string. Rome returns a number, so `watchBlocks` over a plain `webSocket` transport receives nothing. Pass `poll: true` and `viem` polls over the connection instead (checked with `watchBlocks`), or use `ethers` for push updates.

```javascript
import { createPublicClient, webSocket } from "viem";

const client = createPublicClient({
  transport: webSocket("wss://hadrian.testnet.romeprotocol.xyz/"),
});

const unwatch = client.watchBlocks({
  poll: true,
  onBlock: (block) => console.log("new block", block.number),
});

client.watchEvent({
  poll: true,
  address,
  onLogs: (logs) => console.log(logs),
});
```

## Raw JSON-RPC

```json
→ {"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]}
← {"jsonrpc":"2.0","id":1,"result":8704545703806774}            // subscription id
← {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":8704545703806774,"result":{…block…}}}
```

The subscription id is a JSON number, not a hex string, and the notifications carry the same number. `ethers` handles this for you. `viem`'s `webSocket` transport only accepts a string id, so it drops the notifications (see [viem](#viem)). If you write a raw client, compare ids as numbers and pass the number back to `eth_unsubscribe`.

For `logs`, pass a filter object as the second param:

```json
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["logs",{"address":"0x…","topics":["0x…"]}]}
```

## Notes

* Subscriptions push updates as new blocks are produced (latency is on the order of a block time).
* For long-lived connections, handle reconnects in your client — standard for any WebSocket RPC.


# Call Solana from EVM

Call Solana programs from Solidity via Rome's CPI and helper precompiles.

Rome's precompiles let Solidity contracts call Solana programs directly. This guide covers the mechanics.

## Prerequisites

* The Rome Solidity interfaces from the public [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo (installed from GitHub, not npm — see [Deploy Solidity](/developer-guides/deploy-solidity#using-the-rome-solidity-sdk)) — precompile interfaces live in [`contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol)
* A deployed Rome contract (see [Deploy Solidity](/developer-guides/deploy-solidity))

Bind each interface to its precompile address:

```solidity
import {ICrossProgramInvocation, ISystemProgram, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

ICrossProgramInvocation constant CpiProgram    = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
ISystemProgram          constant SystemProgram = ISystemProgram(0xFF00000000000000000000000000000000000007);
IHelperProgram          constant Helper        = IHelperProgram(0xFF00000000000000000000000000000000000009);
```

## The CpiProgram precompile

`CpiProgram` (`0xFF…08`) dispatches CPI (`invoke` / `invoke_signed`) plus cross-state read shortcuts (`account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive`):

```solidity
// Call a Solana program
CpiProgram.invoke(programId, accounts, instructionData);

// Call with PDA signing: your contract's external-authority PDA signs, plus one
// salted sub-PDA per entry in `salts` (Rome derives ["EXTERNAL_AUTHORITY", contract, salt])
CpiProgram.invoke_signed(programId, accounts, data, salts);
```

## Transfer lamports

For simple lamport and SPL transfers, use the HelperProgram precompile — no hand-built CPI needed. A contract that calls it acts as **itself**: the transfers below move the contract's own lamports and tokens (its external-authority PDA signs), not the user's. For a contract to pull a user's SPL tokens into a token account it controls, and know whose they are, the user grants an SPL delegation. An ERC-20 `transfer` or `transferFrom` to a contract only credits the wrapper's escrow ledger and gives the contract no token account to spend from — see [Integrate a Solana program](/developer-guides/integrate-solana-program).

```solidity
contract Transfers {
    IHelperProgram constant Helper = IHelperProgram(0xFF00000000000000000000000000000000000009);

    // Transfer lamports from this contract's PDA to an EVM address's PDA
    function transferSol(address to, uint64 lamports) external {
        Helper.transfer_lamports(to, lamports);
    }

    // Transfer an SPL token from this contract's PDA-owned token account
    function transferSpl(address to, uint64 tokens, bytes32 mint) external {
        Helper.transfer_spl(to, tokens, mint);
    }

    // Create the caller's ATA for a mint
    function createAta(bytes32 mint) external {
        Helper.create_ata(msg.sender, mint);
    }
}
```

`transfer_spl` has several overloads (including a delegate variant for `transferFrom` flows); see `interface.sol`. On the cached track, use `ISplCached` (`0xff…05`) / `IAssociatedSplCached` (`0xff…06`) instead — a contract uses one track consistently, and only the legacy track can `invoke` a custom program (see [Integrate a Solana program](/developer-guides/integrate-solana-program)).

## Reading account data

Read any Solana account's data via the CpiProgram read shortcuts:

```solidity
(
    uint64 lamports,
    bytes32 owner,
    bool isSigner,
    bool isWritable,
    bool executable,
    bytes memory data
) = CpiProgram.account_info(accountPubkey);
```

## PDA derivation

Find Program Derived Addresses from Solidity via the System precompile:

```solidity
ISystemProgram.Seed[] memory seeds = new ISystemProgram.Seed[](2);
seeds[0] = ISystemProgram.Seed("my-program-seed");
seeds[1] = ISystemProgram.Seed(abi.encodePacked(someValue));

(bytes32 pda, uint8 bump) = SystemProgram.find_program_address(targetProgramId, seeds);
```

## Base58 conversion

Convert between `bytes32` and base58 (Solana's address format):

```solidity
bytes memory base58Str = SystemProgram.bytes32_to_base58(pubkey);
bytes32 pubkey = SystemProgram.base58_to_bytes32(base58Bytes);
```

## Calling custom Solana programs

To call any Solana program, build the account list and instruction data yourself:

```solidity
contract CustomCPI {
    ICrossProgramInvocation constant CpiProgram = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
    bytes32 constant MY_PROGRAM = 0x0000000000000000000000000000000000000000000000000000000000000000; // your Solana program ID

    function callMyProgram(bytes32 account1, bytes32 account2, bytes calldata ixData) external {
        ICrossProgramInvocation.AccountMeta[] memory accounts = new ICrossProgramInvocation.AccountMeta[](2);
        accounts[0] = ICrossProgramInvocation.AccountMeta(account1, false, true);
        accounts[1] = ICrossProgramInvocation.AccountMeta(account2, false, false);

        CpiProgram.invoke(MY_PROGRAM, accounts, ixData);
    }
}
```

## Key Constraints

1. **All accounts must be declared upfront.** The Solana transaction must include every account the CPI will touch — dynamic account discovery inside CPI is not possible.
2. **CPI depth limit: 4 levels.** Rome EVM → your target → the target's call → one more. Plan your call depth.
3. **Solana pubkeys are `bytes32`,** not 20-byte Ethereum addresses.
4. **Instruction data is raw bytes** in the format the target program expects (typically Borsh, little-endian).

## What's Next

* **See it in a real app** — [cardo](https://github.com/rome-protocol/cardo) sends CPI calls to the precompile straight from the user's wallet, with no adapter contract; [rome-dex](https://github.com/rome-protocol/rome-dex) (dual-lane AMM) does the same and also has a custody-less router contract; [aerarium](https://github.com/rome-protocol/aerarium) is a lending app that does not call Solana programs from Solidity.
* [Integrate a Solana program](/developer-guides/integrate-solana-program) — the user's Solana identity, signing as it, the one-track rule, and account provisioning for a real program on the other side
* [Precompile reference](/reference/precompiles) — every selector on every precompile
* [Call EVM from Solana](/developer-guides/call-evm-from-solana) — the reverse: drive EVM contracts from a Solana wallet
* [Token Interop](/core-concepts/token-interop) — how ERC-20 and SPL tokens work together
* [Constraints](/core-concepts/constraints) — CPI depth and other limits


# 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): 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) 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) 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)):

* `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) and [Compute Budget](/core-concepts/compute-budget).

## 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).

## What's Next

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


# Integrate a Solana program

Integrate a Solana program with Rome EVM — who the Solana signer is, how value reaches your program and comes back, the one-track rule, atomic vs iterative, and account provisioning.

This guide is for a Solana program team that wants Rome EVM users to use their program — a privacy, lending or vault program that should hold value for EVM accounts. It builds on [Call Solana from EVM](/developer-guides/call-solana-from-evm) (the precompile mechanics) and answers the questions that come up once a real program is on the other side: *who* signs, *how* tokens get in and out, and *which* limits shape the design.

The flow used throughout: an EVM account moves USDC into your program ("shield" or "deposit"), your program holds it for that user, and later releases it back ("unshield" or "withdraw"). Replace the verbs with your own.

## 1. Every EVM address has one Solana identity

Rome derives a Solana account for every EVM address — users and contracts alike — called its **external authority**:

```
external_auth(addr) = find_program_address(["EXTERNAL_AUTHORITY", addr_h160], rome_evm_program_id)
```

It owns the address's associated token accounts (that is where `wUSDC.balanceOf(addr)` reads from) and it is the key that signs CPIs made on that address's behalf. Your program sees a 32-byte Solana pubkey; it never sees an H160.

**Who signs is decided by who calls the precompile:**

| The precompile is called by                                          | The Solana signer                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A **contract**, with a normal `CALL`                                 | The **contract's own** external authority (`external_auth(contract)`), plus any salted sub-PDAs of the contract it asks for                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| A **user**, in a transaction sent straight to the precompile address | The **user's own** external authority                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| A contract via `DELEGATECALL`                                        | The bare authority is refused (`DelegatecallOwnerAuthority`). `invoke` without a signer, and `invoke_signed` with salted sub-PDAs, still run — but those sub-PDAs are namespaced by the running contract's address (`["EXTERNAL_AUTHORITY", caller, running_contract, salt]`), so `pda_with_salt` does not derive them. Of HelperProgram only the creation and funding selectors pass (`create_ata*`, `create_pda*`, `swap_gas_to_lamports`, `init_spl_mint`, `create_and_init_mint`); they are exempt because their target is bound to the caller or funded by the operator, not because they sign nothing |

The consequence is the ERC-20 rule you already know: **a contract cannot move a user's assets by itself.** It acts as its own identity, and the user grants it rights first. For tokens that have to move on the Solana side, that grant is an SPL delegation on the user's token account; the wrapper's ERC-20 `approve` is only an EVM-side allowance that the wrapper's own transfers honour. That is how Rome's own token wrappers work, and it is how your adapter will work (section 3).

Salted sub-PDAs: `invoke_signed` takes a list of `bytes32` salts, and Rome signs with `["EXTERNAL_AUTHORITY", caller_h160, salt]` for each (bump appended; the caller is the contract on a direct call). `HelperProgram.pda_with_salt(addr, salt)` returns the address from Solidity. Use one salt per user (for example the user's address) when your program wants a distinct owner account per position.

Funding: an external authority PDA does not have to exist to own an ATA (`create_ata` is idempotent and the transaction's Solana fee payer covers the rent). It must exist and hold lamports only when it funds accounts your instruction creates. `HelperProgram.create_pda(addr)` creates it as a plain System `CreateAccount`, so call it once; it is not idempotent. `swap_gas_to_lamports(n)` credits the address's PDA with `n` lamports from the transaction's Solana fee payer, and the caller pays for them in Rome gas: every legacy-track CPI's effect on the fee payer's balance is added to the transaction fee and charged to the user at the gas price, the same way rent for accounts a CPI creates is charged. The Solana lane's provisioning step is described in [Call EVM from Solana](/developer-guides/call-evm-from-solana); a contract provisions its own PDA in its constructor or first call.

## 2. Three ways to hold a user's position

**A. An adapter contract (recommended for a reusable adapter).** One Solidity contract fronts your program. The user grants the adapter's authority an SPL delegation on their token account; the adapter pulls the tokens into a token account it owns, calls your program as itself, and keys the user's position inside your program by a per-user sub-PDA or by an identifier it passes in the instruction data. On the way out it tells your program to release to the user's own token account. The adapter is the only Solana identity your program needs to know. The grant is an SPL delegation, not an ERC-20 `approve`; section 3 explains why and what it costs the user.

**B. The user as the Solana signer.** The front end sends a transaction from the user's EVM account **directly to the CpiProgram precompile** (`to = 0xff…08`, calldata = the ABI-encoded `invoke` or `invoke_signed`), so the user's own external authority signs and your program keys the position by it. No adapter contract and no delegation, but each action is an opaque calldata transaction in the wallet, and anything the flow needs besides the CPI (the ATA, a prior transfer) is a separate transaction. It is a fit for one-time grants (a user's `approve_spl` to a wrapper) and for simple programs, not for multi-step flows. [Cardo](https://github.com/rome-protocol/cardo) uses this shape: its front end sends the `invoke` straight to the precompile from the user's wallet.

**C. A custody-less router.** A contract that never holds the user's tokens. The user grants the router's authority an SPL delegation on their own token account; the router CPIs into your program with that authority as the transfer authority, so tokens move from the user's token account to your program's account (or back to the user's), and the router holds nothing between calls. The router derives every user-side account from `msg.sender`, so one user's grant cannot be pointed at another user's account, and it calls one fixed program with fixed instruction shapes rather than acting as a general `invoke` passthrough. The `RomeDexRouter` in [rome-dex](https://github.com/rome-protocol/rome-dex) is written this way; rome-dex's EVM lane also offers the raw direct `invoke` of shape B. It needs the same grant as A, with the same one-delegate cost (section 3), and no pull step.

The rest of this guide follows A; C uses the same grant and the same signing rule.

## 3. Tokens in, tokens out

USDC on Rome is one SPL mint seen from two sides: the `wUSDC` ERC-20 wrapper on the EVM side and the external authority's ATA on the Solana side. For a user's own address the two are the same balance: `wUSDC.balanceOf(user)` reads the user's ATA. There is no bridge or wrapping step in the middle.

**A contract's wrapper balance is not a token account the contract controls.** The factory deploys `SPL_ERC20_cached` for an ordinary mint, and Hadrian's `wUSDC` is one. That wrapper cannot sign for a contract, so when `wUSDC.transferFrom(user, adapter, amount)` or `transfer` sends tokens to a contract, the SPL tokens move into the **wrapper's own escrow token account** (`escrow_ata()`), and the wrapper records the amount per contract in its storage. `wUSDC.balanceOf(adapter)` returns that record, an escrow ledger, not the balance of a token account owned by `external_auth(adapter)`. Your adapter cannot hand those tokens to your program in a CPI; they can only leave through the wrapper again. So an adapter that funds itself with `transferFrom` has nothing it can spend on the Solana side.

The path that works is an SPL delegation:

* **Grant.** The user sends `approve_spl(adapter, amount, mint)` as a transaction **straight to the HelperProgram** (`to = 0xff…09`), where `mint` is the wrapper's `mint_id()`. The user's own authority signs, and the delegate it sets on the user's token account is the adapter's external authority.
* **In.** The adapter's `deposit(amount)` pulls with the delegate overload `HelperProgram.transfer_spl(srcAta, dstAta, amount, mint)`, with the user's ATA as source and the adapter's own ATA (`HelperProgram.ata(address(this), mint)`, created once with `create_ata`) as destination. The adapter's authority signs as the delegate. The tokens are now in a token account owned by `external_auth(adapter)`, which is the account your CPI names; the signer and the source are the adapter's own, so no further permission is needed. They do not show up in `wUSDC.balanceOf(adapter)`.
* **Out.** Your program's withdraw instruction transfers to the **user's** ATA, `HelperProgram.ata(user, mint)`. Receiving needs no signature from the user, and the balance is immediately visible as `wUSDC.balanceOf(user)`. If the ATA may not exist yet, create it first with `HelperProgram.create_ata(user, mint)` (idempotent).

**A token account has one delegate.** The grant above replaces the wrapper's own grant on that account. Until the user grants the wrapper again, every `wUSDC` `transfer` or `transferFrom` out of their address reverts with SPL error `0x4`. After any flow that uses this grant, call `wUSDC.isEnabled(user)`; when it returns `false`, ask the user to send `approve_spl(wrapper, 2^64-1, mint)` to the HelperProgram again, the same maximum grant the wrapper is built around (see [One-time: grant the wrapper](/developer-guides/dual-lane-app#one-time-grant-the-wrapper)). A capped grant would break the user's ordinary `wUSDC` transfers again once it runs low, and `isEnabled` stays `true` while any of it remains, so the app would not know to ask again.

To check which wrapper a mint has, read the factory's `wrapper_kind_by_mint(mint)` (the factory address is in the chain's `contracts.json` in the [registry](https://github.com/rome-protocol/rome-registry)). `1` is the cached wrapper this section describes. `2` is the direct-CPI wrapper the factory deploys for a Token-2022 mint with an armed transfer hook; this guide does not cover it. The registry's `tokens.json` lists the wrapper address and mint but not which kind it is.

## 4. One track per contract, and per transaction

Rome has two families of precompiles that change Solana state, and **a transaction uses exactly one of them** — the runtime refuses a transaction that mixes mutating calls from both, even across contracts.

| Track      | Precompiles                                                                                         | Effects land                                                                                          | Works in iterative execution                                                              |
| ---------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Legacy** | `CpiProgram` `0xff…08` (`invoke`, `invoke_signed`), `HelperProgram` `0xff…09`, `Withdraw` `0x42…16` | Immediately, as a real CPI inside the EVM step                                                        | **No** — a legacy CPI in an iterative transaction fails with `CpiProhibitedInIterativeTx` |
| **Cached** | `SplCached` `0xff…05`, `ASplCached` `0xff…06`, `SystemCached` `0xff…04`, `WithdrawCached` `0xff…0b` | Staged in an overlay and committed as CPIs at the end of the transaction; an EVM revert discards them | Yes                                                                                       |

What this means for a program integration:

* **Calling your own program's instructions is legacy-track only.** The cached track has no generic `invoke`; it covers SPL transfers, ATA creation, system-program operations and gas withdrawal.
* So the step that funds the adapter must be legacy-track too if it runs in the same transaction as the CPI. The cached wrapper's `transferFrom` is a cached-track call, so it cannot share a transaction with your CPI (and, as section 3 shows, it would only credit an escrow ledger). Pull with `HelperProgram.transfer_spl(userAta, adapterAta, amount, mint)` instead, after the user has made the adapter the SPL delegate (`approve_spl`, sent by the user directly to the HelperProgram). The pull and the CPI are both legacy-track, so they run in one transaction.
* **Keep the wrapper out of that transaction.** Do not call the wrapper's mutating functions (`wUSDC.transfer`, `transferFrom`) from the adapter in the same transaction as a legacy CPI. Check `isEnabled(user)` and balances from the front end with `eth_call`.
* **And the CPI transaction must be atomic.** That is the bound the design sits under.

Reads are outside the rule: `account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive` and `HelperProgram.user_balance` / `allowance_of` are reads of pre-loaded accounts (no CPI), allowed on either track and in both execution modes. Put your pre-checks on them. One ordering rule: once a cached mutation has fired in a transaction, legacy-track reads are blocked for the rest of it — read first, then mutate.

## 5. Atomic is the budget

An atomic Rome transaction is one Solana transaction: about **1.4M compute units**, a **64-address** account list and a **4 KB** wire size (Solana v1 transactions). The Rome program and your CPI share that budget. A wrapper transfer runs about 170K CU on Hadrian and your instruction adds its own; measure with the Solana receipt's `computeUnitsConsumed`, not EVM `gasUsed`.

When emulation decides a transaction does not fit, Rome runs it **iteratively** across several Solana transactions, and a legacy CPI inside it then fails. Keep each of deposit and withdraw to one token movement plus one CPI into your program, and keep the Solidity around them small. Split anything larger into two user transactions.

CPI depth: Solana allows 4 levels and Rome uses one, so your instruction runs at level 2 with two levels below it (your program → SPL Token leaves one).

## 6. Account provisioning

Your program does not receive its accounts from Solidity at run time. The proxy **emulates** every transaction before sending it, records every Solana account the CPIs touched, and places them in the Solana transaction. Your adapter names the accounts in the `invoke_signed` account list; emulation proves they resolve. Three things follow:

* Every account your instruction touches must be derivable from inputs the adapter has (its own PDA and sub-PDAs, your program ID, mints, the user's address). `SystemProgram.find_program_address` and `CpiProgram.pdas_batch_derive` do the derivations in Solidity.
* An account that does not exist yet (a fresh position, a fresh ATA) still has to be named in the account list; its address is what reaches the Solana transaction.
* Everything the transaction touches — including Rome's own accounts for the user, the adapter and the wrapper — must stay under 64 addresses. A design that touches ten markets in one call will not fit.

For the Solana-wallet lane, `rome_emulateCallAccounts` exposes the same discovery to a client; the SDK uses it.

Two accounts a CPI may never name: the Solana fee payer that submits the transaction (except inside a plain System transfer instruction) and the Rome program itself. A CPI back into the Rome program is refused.

## 7. A deposit and a withdraw, in Solidity

A minimal sketch of an adapter on the legacy track. The user has already sent `approve_spl(adapter, amount, mint)` to the HelperProgram (section 3). `PRIVACY_PROGRAM` is your program ID; `encodeDeposit` / `encodeWithdraw` build your instruction data (Borsh, little-endian); the account metas follow your instruction's layout.

```solidity
import {ICrossProgramInvocation, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

contract PrivacyAdapter {
    ICrossProgramInvocation constant Cpi    = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
    IHelperProgram          constant Helper = IHelperProgram(0xFF00000000000000000000000000000000000009);
    bytes32 constant SPL_TOKEN = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; // TokenkegQ...

    bytes32 immutable PRIVACY_PROGRAM;
    bytes32 immutable USDC_MINT;

    constructor(bytes32 program, bytes32 mint) {
        PRIVACY_PROGRAM = program; USDC_MINT = mint;
        Helper.create_pda(address(this));                 // the adapter's own Solana identity
        Helper.create_ata(address(this), USDC_MINT);      // and a USDC account it owns
    }

    function _salt(address user) internal pure returns (bytes32) { return bytes32(uint256(uint160(user))); }

    function deposit(uint64 amount) external {
        bytes32 self       = Helper.pda(address(this));                   // signer: the adapter's authority
        bytes32 adapterAta = Helper.ata(address(this), USDC_MINT);
        bytes32 position   = Helper.pda_with_salt(address(this), _salt(msg.sender)); // per-user sub-PDA, also signs

        // Pull from the user's token account. The adapter's authority signs as the SPL
        // delegate the user granted; this is a legacy-track call, like the CPI below.
        Helper.transfer_spl(Helper.ata(msg.sender, USDC_MINT), adapterAta, amount, USDC_MINT);

        ICrossProgramInvocation.AccountMeta[] memory accs = new ICrossProgramInvocation.AccountMeta[](5);
        accs[0] = ICrossProgramInvocation.AccountMeta(self,       true,  true);
        accs[1] = ICrossProgramInvocation.AccountMeta(position,   true,  true);
        accs[2] = ICrossProgramInvocation.AccountMeta(adapterAta, false, true);                          // source: owned by the adapter's authority
        accs[3] = ICrossProgramInvocation.AccountMeta(_vaultFor(position), false, true);                 // your program's vault
        accs[4] = ICrossProgramInvocation.AccountMeta(SPL_TOKEN, false, false);

        bytes32[] memory salts = new bytes32[](1);
        salts[0] = _salt(msg.sender);
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeDeposit(amount), salts);
    }

    function withdraw(uint64 amount) external {
        bytes32 position = Helper.pda_with_salt(address(this), _salt(msg.sender));
        bytes32 userAta  = Helper.ata(msg.sender, USDC_MINT);            // destination: the user's own account
        // ... same shape: your program moves `amount` from the vault to userAta, signed by `position`
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeWithdraw(amount), salts);
    }
}
```

The adapter never calls the `wUSDC` contract. The pull is a HelperProgram call that spends the user's delegation, and the tokens sit in a token account that `external_auth(adapter)` owns, which is why the CPI can spend them.

Notes on the signer. Rome signs for exactly two kinds of account: the calling contract's bare authority (`Helper.pda(address(this))`), which signs without being listed in `salts`, and one salted sub-PDA per entry in `salts`. Every salt you list must appear in the account list as a signer, or the call fails with `signer account calculated from the provided seed was not found`. Any other account marked as a signer, including the user's own authority, gets no seed from Rome, and the Solana runtime refuses the CPI. The user's rights over their tokens come from the delegation, never from the adapter signing as the user.

Delegation without a pull: if your program should spend straight from the user's token account, the user sends `HelperProgram.approve_spl_raw_delegate(userAta, yourProgramPda, amount, mint, decimals)` directly to the HelperProgram, naming your program's PDA as SPL delegate. The Rome bridge uses this for Wormhole. The one-delegate rule from section 3 applies here too: the grant displaces the wrapper's.

## 8. Testing

* Deploy against **Hadrian** (testnet, chain 200010) first; addresses and RPC are in [Contract Addresses](/reference/contract-addresses) and the [registry](https://github.com/rome-protocol/rome-registry).
* `eth_call` and `eth_estimateGas` run the same emulation the proxy uses to send, so a CPI that fails there fails on chain for the same reason, and the revert data carries the underlying Solana error (`non-evm call error: …` in the Solana program log).
* Read the Solana side of a landed EVM transaction with `rome_solanaTxForEvmTx(hash)`; the Solana receipt is where CU and your program's logs live.
* A CPI that works in `eth_call` but is refused when sent usually tripped a limit the emulation reports: the 64-address cap, the atomic CU budget (the transaction went iterative), or the track rule.

## 9. Reference

* [Precompile reference](/reference/precompiles) — every selector on every precompile, with its dispatch class and the delegatecall rule.
* [Constraints](/core-concepts/constraints) — the limits above in one table.
* [Token Interop](/core-concepts/token-interop) — wrappers, canonical mints, deposit and withdraw.
* Reference apps in the public repos: [cardo](https://github.com/rome-protocol/cardo) (the user calls the CPI precompile directly, shape B), [rome-dex](https://github.com/rome-protocol/rome-dex) (direct `invoke` plus a custody-less router, shape C), and [aerarium](https://github.com/rome-protocol/aerarium) (a Compound III lending app over one shared market; it does not call Solana programs from Solidity (no CPI), so it is not an example of an adapter).


# Build a dual-lane app

A step-by-step walkthrough of a dual-lane app — one Solidity contract used by both a MetaMask user and a Solana wallet user, with exactly what happens at each step.

A **dual-lane app** is one Solidity contract that both a **MetaMask (EVM)** user and a **Solana wallet** user use directly — same contract, same state, each with the wallet they already have. This page walks through *exactly* what happens at each step, on each side.

The example is a tiny **vault**: you `deposit` USDC and later `withdraw` it. "Stake / unstake", "supply / redeem", "tip / claim" are the same shape.

## What you write — a standard ERC-20 vault

On Rome, a Solana user's USDC appears on the EVM side as an ordinary **ERC-20 token** — the SPL wrapper for that mint (e.g. `wUSDC`). So your contract is a normal token vault: it pulls tokens with `transferFrom` and returns them with `transfer`. Nothing Rome-specific:

```solidity
interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract Vault {
    IERC20 public immutable token;                 // the wUSDC wrapper
    mapping(address => uint256) public balanceOf;
    constructor(IERC20 _token) { token = _token; }

    function deposit(uint256 amount) external {
        require(token.transferFrom(msg.sender, address(this), amount));
        balanceOf[msg.sender] += amount;
    }
    function withdraw(uint256 amount) external {
        balanceOf[msg.sender] -= amount;
        require(token.transfer(msg.sender, amount));
    }
}
```

> **Why ERC-20, not `payable` / `msg.value`?** A Solana user's spendable balance is their **wallet's SPL token account**, surfaced 1:1 as this ERC-20 wrapper — *not* the EVM native balance. So a Solana user can always fund a `transferFrom`, but a `deposit() payable` would need native value they don't hold at rest. Build around the token and both lanes work the same way.

Deploy it with Foundry or Hardhat, pointing the constructor at the wrapper address for your token (from the [registry](https://github.com/rome-protocol/rome-registry)). On Rome the gas token is USDC, so you need a little USDC gas balance to deploy.

## The two lanes

| Lane       | Wallet                                          | How the app calls it                                                     |
| ---------- | ----------------------------------------------- | ------------------------------------------------------------------------ |
| **EVM**    | MetaMask (an EVM key)                           | `submitRomeTx` — the standard Rome write                                 |
| **Solana** | a Solana wallet such as Solflare (a Solana key) | `submitRomeTxV1` — one Solana v1 transaction per call, no EVM key needed |

The EVM lane is ordinary. The rest of this page is the **Solana lane** — the interesting half.

## The key idea: the synthetic is a pass-through

A Solana user's EVM identity is their **synthetic address** — `keccak256(solana_pubkey)[12:]`. It's their `msg.sender` in the contract, but **it holds nothing at rest.** The user's money lives in their **Solana wallet** (as SPL USDC), and on the EVM side that same balance is what `wUSDC.balanceOf(synthetic)` reads. Value flows *through* the synthetic:

* **In** (deposit): wallet token account → synthetic token account → contract (via `transferFrom`).
* **Out** (withdraw): contract → synthetic token account → wallet token account.

The synthetic nets back to nothing after each round-trip.

## One-time: Activate (provision the synthetic)

A brand-new synthetic's on-chain account doesn't exist until you create it. The first time a Solana user acts, their synthetic is **provisioned** with a `create_pda` call — after that, value-moving calls (the ERC-20 `transferFrom`, the sweep) can be signed by it.

`submitRomeTxV1` does not do this for you, so provision explicitly, once, before the first value-moving call. A good place is an "Activate" step, or just before the first approve:

```javascript
import { submitRomeTxV1, isSyntheticProvisioned, buildCreatePdaCall, syntheticAddress } from "@rome-protocol/sdk";

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: a Keypair in scripts, a LaneSigner in a wallet app
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await submitRomeTxV1(deps, buildCreatePdaCall(synthetic)); // one create_pda
}
```

## One-time: grant the wrapper

The wUSDC wrapper moves a user's tokens as the **SPL delegate** of their token account, so each user has to grant it that right once per wrapper, before the first deposit. This applies to a MetaMask user and to a Solana user's synthetic address alike. Without the grant, every `transferFrom` out of the wallet fails and the deposit reverts with SPL error `0x4` (owner mismatch).

Check with the wrapper's view `isEnabled(user)`. If it returns `false`, send `approve_spl(wrapper, 2^64-1, mint)` straight to the Helper precompile at `0xff00000000000000000000000000000000000009`; the mint is `mint_id()` on the wrapper. On the EVM lane that is a `submitRomeTx` call, on the Solana lane a `submitRomeTxV1` call:

```javascript
import { submitRomeTx, submitRomeTxV1, syntheticAddress } from "@rome-protocol/sdk";
import { createPublicClient, encodeFunctionData, http, parseAbi } from "viem";

const HELPER = "0xff00000000000000000000000000000000000009";
const wrapperAbi = parseAbi(["function isEnabled(address user) view returns (bool)", "function mint_id() view returns (bytes32)"]);
const grantAbi = parseAbi(["function approve_spl(address spender, uint64 amount, bytes32 mint)"]);

const publicClient = createPublicClient({ transport: http(rpcUrl) }); // a viem client on the chain's RPC

// who: the MetaMask address, or syntheticAddress(wallet.publicKey) for a Solana user
let grant = null; // stays null when isEnabled is true
if (!(await publicClient.readContract({ address: wUSDC, abi: wrapperAbi, functionName: "isEnabled", args: [who] }))) {
  const mint = await publicClient.readContract({ address: wUSDC, abi: wrapperAbi, functionName: "mint_id" });
  grant = { to: HELPER, data: encodeFunctionData({ abi: grantAbi, functionName: "approve_spl", args: [wUSDC, 2n ** 64n - 1n, mint] }) };
}
// EVM lane:    if (grant) await submitRomeTx(provider, { from: who, ...grant });
// Solana lane: if (grant) await submitRomeTxV1(deps, grant);
```

## What each side needs

|                         | Needs                                                             | Why                                                                                                                        |
| ----------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Solana user**         | **SOL** (a little)                                                | pays the Solana transaction fee on each lane tx                                                                            |
|                         | **USDC as an SPL token** in their wallet                          | the value they deposit (seen on the EVM side as `wUSDC`)                                                                   |
| **EVM user (MetaMask)** | **USDC** as their Rome gas balance                                | gas, and the value they deposit once it is wrapped; they top it up by **bridging USDC** into Rome (there is no faucet)     |
|                         | a **wrap** of some of that gas into **wUSDC** (`withdraw_to_ata`) | the vault takes wUSDC, the ERC-20 wrapper, not native gas. A MetaMask balance of gas USDC is not wUSDC until it is wrapped |
|                         | the one-time **wrapper grant** (`approve_spl`)                    | lets the wUSDC wrapper move their tokens; see [One-time: grant the wrapper](#one-time-grant-the-wrapper)                   |
| **You (the builder)**   | a wallet with **USDC** gas on Rome                                | to deploy the contract                                                                                                     |

## Value IN — deposit (step by step)

The Solana user has SOL + USDC in their Solana wallet. Every step is a Solana v1 transaction, **signed by that wallet**, and the app sends it to the Solana RPC. The fund leg is built in the app and the wallet signs it with one prompt; each contract call is composed by the proxy, and the wallet signs it after a readable authorization message:

1. **Fund leg** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Creates the synthetic's USDC token account (if needed) and runs **`ActivateAta`**, moving `amount` of USDC from the wallet's token account **into the synthetic's**. Now `wUSDC.balanceOf(synthetic)` shows that balance.
2. **Grant the wrapper** (when `isEnabled` is false) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. See [One-time: grant the wrapper](#one-time-grant-the-wrapper). *(Provision the synthetic before this if it is the user's first lane call.)*
3. **Approve** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Lets the vault pull the tokens.
4. **Deposit** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. The vault runs `transferFrom(synthetic, vault, amount)` — the USDC moves from the synthetic into the vault, credited to the synthetic's address.

**Net effect:** USDC went **Solana wallet → (synthetic) → the vault.**

```javascript
import { syntheticAddress, buildFundLeg, submitSolanaInstructionsV1, submitRomeTxV1 } from "@rome-protocol/sdk";
import { encodeFunctionData, erc20Abi } from "viem";

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: a LaneSigner (payer, signAuthHash, signV1Wire) or a Keypair

// 1) fund leg — wallet USDC → synthetic token account (the wallet signs)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) grant the wrapper, once (the synthetic is already provisioned); `grant` is the call built under "One-time: grant the wrapper", or null when isEnabled is true
if (grant) await submitRomeTxV1(deps, grant);

// 3) approve the vault
await submitRomeTxV1(deps, { to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });

// 4) deposit — the vault pulls via transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Value OUT — withdraw (step by step)

Now the user withdraws. Also wallet-signed, two prompts per call:

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` runs `transfer(synthetic, amount)` — USDC moves from the vault back into the **synthetic's** token account.
2. **Sweep leg** — `buildSweepLeg(...)` gives you the `HelperProgram.transfer_spl` call + accounts; run it (create the wallet's token account if needed with `submitSolanaInstructionsV1`, then send the Helper call through `submitRomeTxV1`; the proxy finds the token accounts) to move the USDC from the synthetic **back to the user's own Solana wallet**. The synthetic nets to nothing.

**Net effect:** USDC went **the vault → (synthetic) → the user's Solana wallet.** Nothing is stranded.

```javascript
import { buildSweepLeg } from "@rome-protocol/sdk";

// 1) withdraw — vault returns USDC to the synthetic's token account
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) sweep leg — synthetic token account → the user's own wallet token account
const sweep = buildSweepLeg({ programId, mint: usdcMint, amount, wallet: wallet.publicKey, synthetic });
await submitSolanaInstructionsV1([sweep.ensureWalletAtaIx], { connection, signer });
await submitRomeTxV1(deps, { to: sweep.helperTo, data: sweep.calldata });
```

## Measured on Hadrian

Each Solana-lane step below ran as one version-1 transaction on Hadrian:

| Step                          | Compute units | Bytes | Accounts |
| ----------------------------- | ------------- | ----- | -------- |
| Provision (`create_pda`)      | 39,008        | 703   | 9        |
| Wrapper grant (`approve_spl`) | 50,687        | 899   | 13       |
| Approve                       | 79,335        | 769   | 10       |
| Vault deposit                 | 242,880       | 1,070 | 20       |
| Vault withdraw                | 280,903       | 1,037 | 19       |
| Sweep (`transfer_spl`)        | 46,810        | 899   | 13       |

## The gotchas

These are the things a hand-built Solana-lane transaction gets wrong. The SDK and the proxy's composer handle most of them:

* **Provisioning.** A fresh synthetic's account must be created (`create_pda`) before any value-moving call, or it can't sign the transfer. `submitRomeTxV1` does not do it for you: check `isSyntheticProvisioned` and submit `buildCreatePdaCall(synthetic)` once.
* **Grant the wrapper once.** The wrapper moves tokens as the user's SPL delegate. Check `isEnabled` and send `approve_spl` to the Helper precompile before the first deposit, or the deposit reverts with SPL error `0x4`.
* **Spend the wrapper, not `msg.value`.** A Solana user's balance is their SPL token account, surfaced as the ERC-20 wrapper — move it with `transfer` / `transferFrom`, never native value.
* **Composed for you.** ComputeBudget, the treasure wallet and account discovery are done by the proxy's composer, so none of them appear in your code. Before the wallet signs, the SDK checks the composed transaction and refuses anything but the call the user authorized.
* **Where it's sent.** The wallet signs the v1 transaction and the **app sends it to the Solana RPC** — not to the proxy, which composes it (a relayed call is the one exception, below; see [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls)). On-chain, the program derives `msg.sender` from the Solana signer.
* **Two prompts per call.** The wallet signs a readable authorization message (`Rome authorization` and the EVM transaction hash), then the transaction, both with the same key.
* **Check your wallet signs v1.** `signV1Wire` must return the 64-byte signature over the v1 message; through Wallet Standard that is `solana:signTransaction` on the raw wire and `signedTransaction.slice(-64)`. Not every wallet decodes v1 transactions yet, so test yours before you build on it. On devnet today, Solflare signs them; Phantom does not yet.
* **Big calls relay.** A call that does not fit one transaction is relayed by the proxy with the same authorization signature, and the result has `route: "relay"`. A relayed call is paid in the chain's native gas, so top up the synthetic with `fundSyntheticGas(deps, amountWei)`. See [Relayed calls and long calls](/developer-guides/call-evm-from-solana#relayed-calls-and-long-calls).
* **Gas is USDC.** No faucet — bridge USDC in (see [Getting funded](/resources/faucets)).

The earlier `submitRomeTxSolanaLane` is still in the SDK: it builds a legacy Solana transaction in the client (one wallet prompt, with ComputeBudget and the treasure wallet handled by the SDK).

## The same app from MetaMask

An EVM user calls the identical contract with `submitRomeTx` — standard EVM tooling, gas in USDC. One difference from the Solana user: a MetaMask user holds **native gas USDC**, not wUSDC. The vault takes the wUSDC wrapper, so the user wraps some of their gas first, then grants the wrapper once, then does the ERC-20 steps as usual. There are no fund or sweep legs, because the wrapped tokens already live at their EVM address.

1. **Wrap gas into wUSDC** — send `withdraw_to_ata(wei)` to the Withdraw precompile at `0x4200000000000000000000000000000000000016`. It burns that much native gas and credits the equivalent amount to the user's token account in the gas token, which `wUSDC.balanceOf(user)` then shows. `wei` is in the 18-decimal gas unit and must be a multiple of 10^12 (one 6-decimal wUSDC unit), or the call reverts. Skip this step if the user already holds wUSDC.
2. **Grant the wrapper** (when `isEnabled` is false) — the same `approve_spl` call as above, sent with `submitRomeTx`. See [One-time: grant the wrapper](#one-time-grant-the-wrapper).
3. **Approve** — `approve(vault, amount)` on wUSDC.
4. **Deposit** — `deposit(amount)` on the vault. Withdraw is `withdraw(amount)`.

```javascript
import { submitRomeTx } from "@rome-protocol/sdk";
import { encodeFunctionData, erc20Abi, parseAbi } from "viem";

const WITHDRAW = "0x4200000000000000000000000000000000000016";
const withdrawAbi = parseAbi(["function withdraw_to_ata(uint256 wei_)"]);

// 1) wrap some gas USDC into wUSDC (weiAmount is in 18-decimal gas units)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) grant the wrapper, once: `grant` is the call built under "One-time: grant the wrapper", or null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) approve and 4) deposit, as for any ERC-20
await submitRomeTx(provider, { from: who, to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });
await submitRomeTx(provider, { from: who, to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

Both users share the same `balanceOf` state in the vault.

## What's next

* [Call EVM from Solana](/developer-guides/call-evm-from-solana) — the Solana-lane mechanics in detail
* [Call Solana from EVM](/developer-guides/call-solana-from-evm) — the other direction (CPI)
* [Getting funded](/resources/faucets) — USDC as the gas token; bridge it in


# From home: reach Rome from another chain

Let users on another chain reach your Rome app without leaving home — bring assets in, act, take them back out.

Your users don't have to move to Rome. From their home chain — an L2 like Arbitrum or Monad, or Solana — they bring assets in, use your Rome app, and take assets back out. The bridge is on-chain and authorized by the user's own signature; you wire your app to it through one API.

## How it works

* **Inbound (home → Rome):** on the gas-token USDC route over CCTP, the `settle_inbound_bridge_v2` instruction of the Rome program credits the user, under a signed **EIP-712** `SettleAuthorization`, so there is no privileged settler. The other inbound routes do not use that instruction or signature. Wrapper USDC, ETH over Wormhole and SOL land directly as the wrapper token in the user's account. Solana USDC on a chain whose gas token is USDC needs one more transaction, signed by the user, to turn the wrapper into gas. The quote tells you which signatures a route needs.
* **Outbound (Rome → home):** `RomeBridgeWithdraw` (in [`rome-solidity`](https://github.com/rome-protocol/rome-solidity)) burns the asset on Rome. The user then claims it on the home chain with their own transaction.
* **Transport:** Circle **CCTP** for USDC and the **Wormhole** token bridge for ETH. Solana home chains use plain SPL transfers in and a Rome withdraw out. Wormhole is the inbound rail for ETH only. A generic Wormhole route exists for outbound only, and each asset on it has to be enabled by the Rome operators first.
* **Orchestration:** the off-chain [`rome-bridge-api`](https://github.com/rome-protocol/rome-bridge-api) quotes a route, verifies the source-chain transaction, and sponsors the settle fee. It holds no funds and no keys. It can only trigger what the user already signed.

## Build it

The API is public on devnet at `https://bridge-api.devnet.romeprotocol.xyz`. `GET /v1/routes` lists every live route (asset, rail, direction, source chain, Rome chain, speeds). The calls below are the same four the SDK wraps in `@rome-protocol/sdk/bridge` (`requestQuote`, `registerTransfer`, `getTransfer`, `reportStep`). The CLI runs the whole flow for you: `rome fund` and `rome bridge` for the transfers and `rome verify --path from-home` for a round trip, as described on the [Rome CLI](/products/rome-cli) page.

### Bring assets in

1. **Quote the route.** Send the user's source chain, asset, amount and addresses. Chains go in as CAIP-2 ids such as `eip155:84532`; `romeChainId` is your Rome chain. The older names `ethereum` and `solana` are still accepted but deprecated, and the API marks those responses with a `Sunset` header. The full request and response shapes are in the [API reference](https://github.com/rome-protocol/rome-bridge-api/blob/main/docs/API.md).

   ```bash
   curl -X POST https://bridge-api.devnet.romeprotocol.xyz/v1/quote \
     -H 'Content-Type: application/json' \
     -d '{
       "asset": "USDC",
       "direction": "to-rome",
       "sourceChain": "eip155:84532",
       "romeChainId": "200010",
       "amount": "1000000",
       "sender": { "ethereum": "0xUSER" },
       "recipient": "0xUSER"
     }'
   ```

   Amounts are base-unit integers in strings (`"1000000"` is 1 USDC). ETH amounts are in wei and, on the way out, must be a multiple of 10,000,000,000 wei (the wrapper has 8 decimals); the API rejects anything finer. The response lists the `steps`. The user signs the transactions of every step marked `userSigns: true`, in step order: EVM transactions are in `unsignedTxs`, Solana instructions in `unsignedTx`. Sign them exactly as returned. Steps with nothing to sign are done by the sponsor once Circle or Wormhole has attested the transfer. The outbound claim is the exception, described below: the user builds it.
2. **Have the user sign.** For a CCTP quote that is the USDC approval and the burn on the source chain. If the quote has an entry in `signatureRequests`, the user also signs that EIP-712 `SettleAuthorization` once the burn hash is known; fill in the burn hash and pass the result as `userSettleSig` in the next call. Only the gas-token CCTP route has one. Wormhole ETH, wrapper-intent USDC and Solana routes have none.
3. **Register the transfer.** `POST /v1/transfers` with the full quote and `step1TxHash`, the hash of the last transaction in the first step (the burn). The API checks that the transaction on the source chain matches the quote and returns a transfer `id`.
4. **Poll** `GET /v1/transfers/{id}` every few seconds until `outcome` is `complete`. The user is then funded on Rome and can use your app.

Solana home chains follow the same shape with different steps. The user sends an SPL transfer to their Rome account's token account. SOL arrives as wrapped SOL after that one transfer. On a chain whose gas token is USDC, such as Hadrian, USDC needs a second step: a Rome transaction the user signs that turns the landed wrapper into native gas. Any other SPL token goes through the `spl-solana-to-rome` route, which takes the mint in `splAsset`.

### Take assets back out

Bridging out is not the inbound flow run backwards. It has more than one signature, and the user finishes it on the home chain. Quote it with `direction` set to `from-rome`. `sourceChain` names the non-Rome side as a CAIP-2 id, and the burn goes to that chain. `destinationChainId` is optional and must agree with it when you send it. The older name `ethereum` still means Ethereum Sepolia:

```bash
curl -X POST https://bridge-api.devnet.romeprotocol.xyz/v1/quote \
  -H 'Content-Type: application/json' \
  -d '{
    "asset": "USDC",
    "direction": "from-rome",
    "sourceChain": "eip155:84532",
    "destinationChainId": 84532,
    "romeChainId": "200010",
    "amount": "1000000",
    "sender": { "rome": "0xUSER" },
    "recipient": "0xUSER"
  }'
```

For USDC over CCTP the quote returns two steps, and the user signs both:

1. **On Rome, two transactions in order.** The first grants `RomeBridgeWithdraw` delegate rights over the user's wUSDC on Solana (`approve_spl`). The grant covers exactly the amount of this burn, so the quote includes it every time and the user signs it before each burn. The second burns the wUSDC through `RomeBridgeWithdraw`, which starts the CCTP transfer. Register the transfer with the hash of the burn. The burn spends wUSDC, the token the quote names as `burnToken`, not the native gas USDC. Funds that came in as gas are not what gets burned, so for a round trip bring USDC in with `intent: "wrapper"`.
2. **On the destination chain, the claim.** Poll `GET /v1/transfers/{id}` until step 2 is `ready`. It then carries the `message` and `attestation`. The user calls `receiveMessage(message, attestation)` on Circle's message transmitter on the destination chain (the quote names the transmitter and the CCTP domain). Nothing is sponsored here, so the user needs gas on that chain. Report the claim hash with `POST /v1/transfers/{id}/steps/2`.

ETH out of Rome over Wormhole is not offered on Hadrian or Rubicon today. Hadrian lists ETH over Wormhole toward Rome only. Rubicon lists no ETH route.

Withdrawing to a Solana address has no claim step. USDC is a single Rome transaction that withdraws to the user's Solana account. SOL is one Rome step with three transactions: the same delegate grant over wSOL, an optional creation of the recipient's wSOL token account on Solana (skipped when it already exists), and the withdraw through `RomeBridgeWithdraw`.

**Outbound transfers need no activation step.** A CCTP burn creates a message account on Solana, and that account needs rent. The bridge contract's own Solana account pays that rent. Just before its CPI call, the contract calls `swap_gas_to_lamports`: the transaction's Solana fee payer fronts the lamports, and the user is billed for them in gas in the same burn transaction. A new wallet can make its first outbound transfer with no earlier setup. `RomeBridgeWithdraw` is at version 13.0.0 on Hadrian and on Rubicon today. Outbound quotes carry no activation or top-up step, so your app does not need to check the user's account before the first quote. Inbound transfers need no activation either. The `rome` CLI (0.11.0) still has its own check. `rome bridge --to` (without `--dry-run`) reads the lamports of the user's external-authority account on Solana. If it holds less than 15,000,000 the command stops and tells you to run `rome activate <chain>`, which pays 2 USDC on Hadrian through the `SimpleActivator` contract. `rome verify --path from-home` makes the same check before it starts. The contract does not need that activation.

## Read the code

* [appia](https://github.com/rome-protocol/appia) — a from-home app: users bring USDC from their home chain and use Rome-side DeFi without leaving it.
* [rome-bridge-api](https://github.com/rome-protocol/rome-bridge-api) — the orchestrator (quote → verify → sponsor).

## What's next

* [Getting funded](/resources/faucets) — the same bridge path funds a fresh dev wallet.
* [Deploy Solidity](/developer-guides/deploy-solidity) — put your app on Rome.


# JSON-RPC Support

Ethereum JSON-RPC methods supported by Rome EVM endpoints, plus Rome-specific behaviors — for Foundry, Hardhat, ethers, viem, and MetaMask.

Rome EVM endpoints speak standard Ethereum JSON-RPC. Foundry, Hardhat, ethers, viem, and MetaMask work against Rome the same way they do on any EVM chain. This page lists what's supported and the few Rome-specific behaviors worth knowing.

## Supported methods

**Core (state & execution):** `eth_chainId`, `eth_blockNumber`, `eth_getBalance`, `eth_getCode`, `eth_getStorageAt`, `eth_getTransactionCount`, `eth_call`, `eth_estimateGas`, `eth_gasPrice`, `eth_maxPriorityFeePerGas`, `eth_feeHistory`.

**Blocks & transactions:** `eth_getBlockByNumber`, `eth_getBlockByHash`, `eth_getBlockReceipts`, `eth_getTransactionByHash`, `eth_getTransactionReceipt`, `eth_sendRawTransaction`, block-by-index/count variants.

**Logs & filters:** `eth_getLogs`, `eth_newFilter`, `eth_newBlockFilter`, `eth_getFilterChanges`, `eth_getFilterLogs`, `eth_uninstallFilter`.

**WebSocket subscriptions:** `eth_subscribe` / `eth_unsubscribe` (`newHeads`, `logs`). See [WebSocket Subscriptions](/developer-guides/websocket-subscriptions).

**Net / web3:** `net_version`, `net_listening`, `net_peerCount`, `web3_clientVersion`.

**Small introspection methods:** `rpc_modules` (lists `debug`, `eth`, `net`, `rome`, `rpc`, `txpool`, `web3`), `eth_syncing` (always `false`), `eth_accounts` (always an empty list), `eth_getUncleCountByBlockNumber`, `eth_getUncleCountByBlockHash`, `eth_getUncleByBlockNumberAndIndex` and `eth_getUncleByBlockHashAndIndex` (no uncles, so counts are `0x0` and lookups return `null`), `eth_newPendingTransactionFilter` (accepted, but it never returns hashes because there is no public mempool), and `txpool_status`, which always returns `{"pending":"0x0","queued":"0x0"}` because there is no public mempool.

**Tracing:** `debug_traceRomeTransaction` takes a transaction hash and returns the per-sub-call and per-CPI breakdown for a transaction that bundles several calls into one batch. Ordinary transactions return `null`, as does a hash that is unknown or not indexed yet.

### Not supported

`debug_traceTransaction` / `trace_*` (Ethereum-style transaction tracing; they return `-32601`), `eth_getProof`, `eth_createAccessList`, `eth_blobBaseFee` (no blob transactions), and the `txpool_*` methods other than `txpool_status` (no public mempool). `newPendingTransactions` subscriptions are accepted but never emit.

## Rome extension methods

Beyond standard Ethereum RPC, the proxy exposes `rome_*` methods:

| Method                            | Purpose                                                                                                                                                                                                                     |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rome_emulateTx`                  | Emulate a transaction off-chain (returns the Solana execution result)                                                                                                                                                       |
| `rome_emulateTxWithPayer`         | Emulate against a specific payer                                                                                                                                                                                            |
| `rome_emulateCallAccounts`        | Emulate and return the Solana accounts a call touches                                                                                                                                                                       |
| `rome_emulateRegRollup`           | Emulate a rollup registration                                                                                                                                                                                               |
| `rome_getResources`               | Report proxy resource pool state (payers, holders)                                                                                                                                                                          |
| `rome_mintId`                     | Resolve the SPL mint behind an ERC-20 wrapper                                                                                                                                                                               |
| `rome_buildInfo`                  | Proxy build + on-chain program info                                                                                                                                                                                         |
| `rome_isCompatible`               | Check proxy/program compatibility                                                                                                                                                                                           |
| `rome_solanaTxForEvmTx`           | Get the Solana transaction(s) for an EVM tx hash                                                                                                                                                                            |
| `rome_sendUnsignedTransaction`    | Submit an unsigned transaction (synthetic-sender flows)                                                                                                                                                                     |
| `rome_composeUnsignedAtomicV1`    | Compose one Solana v1 transaction for an unsigned EIP-1559 body authorised by a Solana wallet's Ed25519 signature; the wallet signs it and the app broadcasts it. Refuses when the call does not fit one atomic transaction |
| `rome_composeUnsignedIterativeV1` | Same, but returns every leg (atomic if it fits, else the iterative legs); the wallet signs each and submits them with `rome_submitSignedV1Plan`                                                                             |
| `rome_submitSignedV1Plan`         | Broadcast a signed plan (up to 64 legs); the proxy is transport only, the wallet stays fee payer                                                                                                                            |
| `rome_relayUnsignedV1`            | One wallet signature, the proxy's payer drives the legs; the synthetic pays Rome gas (fund it first with `rome_composeExternalDepositV1`). Returns the EVM tx hash                                                          |
| `rome_composeExternalDepositV1`   | One wallet-signed transaction that credits the synthetic's native gas balance from the wallet's tokens                                                                                                                      |

## Rome-specific behaviors

### Gas is Solana-derived — estimate, don't hardcode

Rome meters execution in Solana compute units, surfaced as EVM gas. Most of what a write costs is creating Solana accounts: a recipient that has never been used, a new token account, a deployed contract. Those accounts are part of the estimate. Practical consequences:

* **Use `eth_estimateGas`, and never hardcode `21000`.** Standard tooling (`forge create`, `cast`, Hardhat, ethers, viem, MetaMask) already calls it and gets a usable value. For calls that touch Solana programs, add a margin (the SDK's `submitRomeTx` pads 1.3x) and set a gas ceiling if the estimate itself reverts. A hardcoded `21000` limit fails whenever the write creates accounts, for example a transfer to an address that has never been used.
* **Rome charges the gas actually used.** In the measurements below the gas used was never more than the estimate, so an estimate that is higher than the work done does not cost you the difference.
* **Leave the margin your tooling adds.** State can change between the estimate and the moment the transaction lands, and the estimate for a call that makes Solana CPIs can come out low. The SDK's `submitRomeTx` pads the estimate by 1.3x as a margin.

Measured on Hadrian, with the gas price at about 10.4 gwei at the time:

| Write                                                  | `eth_estimateGas` | Gas used  |
| ------------------------------------------------------ | ----------------- | --------- |
| Native transfer to a never-used address                | 1,086,961         | 1,086,961 |
| Native transfer to an existing address                 | 21,000            | 10,001    |
| wUSDC transfer to a never-used address                 | 2,544,921         | 2,544,921 |
| Wrap gas into wUSDC (`withdraw_to_ata`)                | 21,000            | 10,001    |
| Wrapper grant (`approve_spl` on the Helper precompile) | 21,000            | 10,001    |
| Deploy a 10-byte contract                              | 1,147,921         | 1,147,921 |

These are single measurements on one chain, not limits. The numbers depend on which accounts a call has to create, so estimate each write instead of copying them.

### `forge script --broadcast` needs `--skip-simulation`

`forge script` sets its broadcast gas limit from a **local** simulation, which under-provisions against Rome's gas model. Add `--skip-simulation` so Foundry uses the chain's `eth_estimateGas` instead:

```bash
forge script script/Deploy.s.sol:Deploy \
  --rpc-url <RPC> --private-key $PRIVATE_KEY --broadcast --skip-simulation
```

`forge create`, `cast send`, and Hardhat are unaffected — they already estimate on-chain.

### `eth_getLogs` is range-capped (\~12,000 blocks)

Like Infura, Alchemy, and other production RPCs, `eth_getLogs` rejects overly wide block ranges (error `-32005`). Bound your queries:

```javascript
const latest = await provider.getBlockNumber();
const logs = await contract.queryFilter(filter, latest - 5000, latest);
```

A `queryFilter` with no range scans all history and will exceed the cap — pass an explicit `fromBlock`/`toBlock`. Receipt-based event assertions (e.g. waiting on a tx and reading its logs) are unaffected.

### Fees

`eth_gasPrice` returns the real, oracle-derived price; wallets (including MetaMask) use it and display correct fees. Submitted fee fields are accepted permissively — both legacy and EIP-1559 transactions land with standard tooling defaults.


# Precompile Reference

Every method on Rome's Solana-interop precompiles — address, Solidity signature, selector, and whether it reads, invokes, or stages a CPI.

The selectors Rome's on-chain program dispatches, by precompile. Selector = `keccak256(signature)[:4]`; every row below is verified against that. The Solidity interfaces live in [`rome-solidity/contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol).

**Read the Dispatch column before anything else.** It says what a method does to Solana state:

| Dispatch              | What happens                                                                                                                                                                                                          | Real Solana CPI | Allowed in iterative execution |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------ |
| `Invoke` / `Composed` | One or more CPIs, signed as the caller's external-authority PDA where seeds apply. On the **legacy** track they run immediately; on the **cached** track they are staged and committed at the end of the transaction. | Yes             | Legacy: **no**. Cached: yes    |
| `EthCall`             | Pure computation or a read of Rome's own state                                                                                                                                                                        | No              | Yes                            |
| `CrossStateEthCall`   | A read of another Solana account's bytes, pre-loaded by emulation. No CPI, no side effects.                                                                                                                           | No              | Yes                            |
| `Precompiled`         | Deterministic cryptography (the Ethereum-standard precompiles)                                                                                                                                                        | No              | Yes                            |

A contract uses **one track**: either the legacy precompiles (`0xff…07`, `0xff…08`, `0xff…09`, `0x42…16`) or the cached ones (`0xff…04`, `0xff…05`, `0xff…06`, `0xff…0b`). Mixing mutating calls from both in one transaction is refused at runtime. Reads stay allowed, with one exception: once a cached-track invoke has fired, legacy-track reads are refused for the rest of the transaction. See [Integrate a Solana program](/developer-guides/integrate-solana-program).

**Who pays.** The Solana fee payer fronts every lamport a legacy-track CPI spends (rent, transfers, `swap_gas_to_lamports`); the program measures the payer's balance around each CPI and bills the net spend to the user as gas at the gas price, refunding lamports a CPI returns.

**Who signs.** A contract that calls a precompile acts as itself: the signer is the contract's external-authority PDA (`["EXTERNAL_AUTHORITY", contract]`). A user's own PDA signs only on a transaction the user sends directly to the precompile address. Under `DELEGATECALL` / `CALLCODE`, mutating selectors are refused except `create_ata*`, `create_pda*`, `swap_gas_to_lamports`, `init_spl_mint` and `create_and_init_mint` on HelperProgram, `create_ata*` on ASplCached, and `invoke` / `invoke_signed` on CpiProgram. They are exempt because their target is bound to the caller or funded by the operator, not because they sign nothing: `create_and_init_mint` still signs with the outer caller's PDA to pay the mint's rent and may only create a mint whose authorities are that PDA, and `invoke` / `invoke_signed` refuse the bare authority (`DelegatecallOwnerAuthority`) and namespace salted PDAs by the running contract's address.

Every method is non-payable (`msg.value > 0` reverts with `TransferProhibited`) except `withdrawal(bytes32)`. Methods marked *single-state only* are refused on op-geth-stack chains. A CPI may not name the Solana fee payer (except inside a plain System transfer instruction) or the Rome program, and may not call the Rome program.

## SystemCached (`0xff00000000000000000000000000000000000004`)

PDA creation + system-program ops (allocate, assign, lamport transfer), staged into overlay. Cached counterpart to legacy `System @ 0xff..07`'s mutating selectors (legacy System retains the pure read selectors).

| Solidity signature                   | Selector     | Dispatch | Description                                                 |
| ------------------------------------ | ------------ | -------- | ----------------------------------------------------------- |
| `create_pda()`                       | `0xe0402a8d` | `Invoke` | Create caller's `external_auth` PDA, rent-exempt, zero-data |
| `create_pda(uint64)`                 | `0x4ceab657` | `Invoke` | Same, with funder-specified lamports above rent             |
| `create_pda(uint64,bytes32)`         | `0x48e2bb86` | `Invoke` | Salt-derived PDA with lamports                              |
| `create_pda(bytes32,uint64,bytes32)` | `0xcc258bbf` | `Invoke` | Owner-specified, size-N, salt-derived                       |
| `allocate(uint64,bytes32)`           | `0x93225c9f` | `Invoke` | Allocate N bytes on a salt-derived PDA                      |
| `assign(bytes32,bytes32)`            | `0x8ac00bdc` | `Invoke` | Assign a salt-derived PDA to a new owner                    |
| `transfer(address,uint64)`           | `0x5d359fbd` | `Invoke` | Lamport transfer to EVM-addr-derived PDA                    |
| `transfer(bytes32,uint64)`           | `0xfd54d1ea` | `Invoke` | Lamport transfer to raw Solana pubkey                       |
| `transfer(bytes32,uint64,bytes32)`   | `0x875abfc0` | `Invoke` | Lamport transfer to raw pubkey, salt-derived source         |

## SplCached (`0xff00000000000000000000000000000000000005`)

SPL Token / Token-2022 transfers + ATA init + cross-state account reads, staged into overlay.

| Solidity signature                              | Selector     | Dispatch            | Description                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------------------------- | ------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transfer(address,uint256)`                     | `0xa9059cbb` | `Invoke`            | Caller-as-owner SPL transfer to EVM-addr-derived PDA's ATA, current rollup mint                                                                                                                                                                                                                                                             |
| `transfer(bytes32,uint256)`                     | `0x6a467394` | `Invoke`            | Same, recipient given as raw Solana pubkey                                                                                                                                                                                                                                                                                                  |
| `transfer(address,uint256,bytes32)`             | `0x57cfeeee` | `Invoke`            | Explicit-mint variant, EVM-addr destination                                                                                                                                                                                                                                                                                                 |
| `transfer(bytes32,uint256,bytes32)`             | `0x7db527f9` | `Invoke`            | Explicit-mint variant, raw-pubkey destination                                                                                                                                                                                                                                                                                               |
| `transferFrom(address,address,uint256,bytes32)` | `0x401e3367` | `Invoke`            | Delegate-source variant — authority = `external_auth(caller)`, accepted as ATA owner OR sufficient-amount delegate. Used by `SPL_ERC20_cached.transferFrom` and router-driven flows                                                                                                                                                         |
| `approve(address,uint256,bytes32)`              | `0x8180f2fc` | `Invoke`            | EVM-spender approve — owner = `external_auth(caller)`, delegate = `external_auth(spender_addr)`. Used by `SPL_ERC20_cached.approve`                                                                                                                                                                                                         |
| `mint(address,uint256,bytes32)`                 | `0x1e458bee` | `Invoke`            | Caller-PDA signs as mint authority. SPL runtime enforces caller-PDA == on-chain mint authority. Used by `SPL_ERC20_cached.mint_to` and inbound bridge settle                                                                                                                                                                                |
| `init(bytes32,bytes32,bytes32)`                 | `0x0b0ad508` | `Invoke`            | `InitializeAccount3` ix (ata, mint, owner)                                                                                                                                                                                                                                                                                                  |
| `account(address)`                              | `0x73b9aa91` | `CrossStateEthCall` | Read SPL account state for EVM-addr's PDA-derived ATA (current rollup mint)                                                                                                                                                                                                                                                                 |
| `account(bytes32)`                              | `0x882358ae` | `CrossStateEthCall` | Read SPL account state for a raw ATA pubkey                                                                                                                                                                                                                                                                                                 |
| `account(address,bytes32)`                      | `0xf9827227` | `CrossStateEthCall` | Same as `account(address)` with an explicit mint.                                                                                                                                                                                                                                                                                           |
| `mint_info(bytes32)`                            | `0xe24bf5d4` | `CrossStateEthCall` | Mint facts, overlay-aware: token program, decimals, **armed** hook program (0 when absent *or* inert), current-epoch fee bps, and an extension-presence bitmap. Same selector as `HelperProgram.mint_info` — one capability, two dispatch homes, because a cached-track contract cannot use the legacy read once a cached invoke has fired. |

## ASplCached (`0xff00000000000000000000000000000000000006`)

Associated Token Account creation, idempotent.

| Solidity signature            | Selector     | Dispatch | Description                                                   |
| ----------------------------- | ------------ | -------- | ------------------------------------------------------------- |
| `create_ata()`                | `0xb6d336ed` | `Invoke` | Idempotent ATA-create for caller-PDA × current rollup mint    |
| `create_ata(bytes32)`         | `0x81972e35` | `Invoke` | Idempotent ATA-create for caller-PDA × explicit mint          |
| `create_ata(address)`         | `0x5a7c3259` | `Invoke` | Idempotent ATA-create for EVM-addr-derived PDA × current mint |
| `create_ata(address,bytes32)` | `0x3de2251a` | `Invoke` | Idempotent ATA-create for EVM-addr × explicit mint            |

## WithdrawCached (`0xff0000000000000000000000000000000000000b`)

Gas-token withdrawal legs (native gas → SPL wrapper, native gas → SOL, native gas → caller PDA), staged. Cached counterpart to legacy `Withdraw @ 0x42..16`.

| Solidity signature            | Selector     | Dispatch   | Description                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------- | ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `withdrawal(bytes32) payable` | `0x4d8b0ea4` | `Composed` | Burn `msg.value` Rome gas, transfer to Solana system-account recipient                                                                                                                                                                                                                                                                                                                                                |
| `withdraw_to_pda(uint256)`    | `0x7f3124a0` | `Composed` | Burn wei, deposit lamports to caller's `EXTERNAL_AUTHORITY` PDA. Single-state only.                                                                                                                                                                                                                                                                                                                                   |
| `withdraw_to_ata(uint256)`    | `0x8059abc0` | `Composed` | Burn wei, deposit SPL wrapper to caller's PDA-owned ATA. Single-state only. Wrap leg of the gas-wrapper bridge.                                                                                                                                                                                                                                                                                                       |
| `deposit(uint256)`            | `0xb6b55f25` | `Composed` | Inverse of `withdrawal` — SPL transfer from caller's PDA-owned ATA to chain's sol\_wallet ATA on Solana side; **mint** `wei` native gas to caller on EVM side. Pure mint (no offsetting debit against `Withdraw::ADDRESS` — the chain's SPL wallet is the economic backing, not the precompile address). Single-state only. Unwrap leg of the gas-wrapper bridge. Matches `HelperProgram.deposit_from_ata` semantics. |

## System (`0xff00000000000000000000000000000000000007`)

PDA derivation + base58 conversion + identity-style utilities. Pure-read precompile — every method is `EthCall` (no Solana side effects).

| Solidity signature                                | Selector     | Dispatch  | Description                                                               |
| ------------------------------------------------- | ------------ | --------- | ------------------------------------------------------------------------- |
| `find_program_address(bytes32,(bytes)[])`         | `0x27e3edda` | `EthCall` | Solana `find_program_address` via syscall — returns `(pda, bump)`         |
| `create_program_address(bytes32,(bytes)[],uint8)` | `0xb99f29e1` | `EthCall` | Constant-time PDA derivation with a known bump                            |
| `rome_evm_program_id()`                           | `0xb76fd45b` | `EthCall` | Returns this Rome EVM program's Solana program ID                         |
| `program_id()`                                    | `0x77764881` | `EthCall` | Returns Rome's System Program ID (this precompile's identity)             |
| `mint_id()`                                       | `0xe132a122` | `EthCall` | Returns the chain's gas-token SPL mint pubkey (`OwnerInfo.chain_mint_id`) |
| `operator()`                                      | `0x570ca735` | `EthCall` | Returns the Solana signer of the outer Rome tx                            |
| `bytes32_to_base58(bytes32)`                      | `0xfa2b1a5f` | `EthCall` | Pubkey-bytes → base58 string                                              |
| `base58_to_bytes32(bytes)`                        | `0x5df01b72` | `EthCall` | base58 string → pubkey-bytes                                              |

## CpiProgram (`0xff00000000000000000000000000000000000008`)

**Dual-purpose precompile.** Of the 7 selectors below, **2 perform real Solana CPI** (`invoke`, `invoke_signed` — dispatch as `Invoke`) and **5 are pure cross-state reads** (`account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive` — dispatch as `CrossStateEthCall`, no syscall, no `found_cpi` flag set). The historical phrase "CPI shortcut selectors" referred to the read selectors as CU-saving replacements for earlier CPI-based reads — they themselves are **not CPIs**. See the disambiguation table at the top of this section for the full behavioral matrix.

| Solidity signature                                             | Selector     | Dispatch            | Description                                                                                                                                                                                                                           |
| -------------------------------------------------------------- | ------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invoke(bytes32,(bytes32,bool,bool)[],bytes)`                  | `0x7480cb86` | `Invoke`            | CPI signed as `EXTERNAL_AUTHORITY(caller)` when that account is listed as a signer (refused under DELEGATECALL); no salted sub-PDAs. Caller passes target program + account metas + ix data                                           |
| `invoke_signed(bytes32,(bytes32,bool,bool)[],bytes,bytes32[])` | `0xb94f3733` | `Invoke`            | CPI signed as `EXTERNAL_AUTHORITY(caller)` on a direct CALL; salts append per-PDA seed. Under DELEGATECALL the bare caller authority is refused and salted seeds carry the running contract's address — see the derivation note below |
| `account_info(bytes32)`                                        | `0xc13465d9` | `CrossStateEthCall` | Read a Solana account's full state (lamports/owner/flags/data)                                                                                                                                                                        |
| `account_data_at(bytes32,uint16,uint16)`                       | `0x593762e8` | `CrossStateEthCall` | Read a slice of any Solana account's data buffer                                                                                                                                                                                      |
| `account_u64_at(bytes32,uint16)`                               | `0xb317d4c1` | `CrossStateEthCall` | Typed u64 LE read at a known offset — saves ABI roundtrip                                                                                                                                                                             |
| `account_lamports(bytes32)`                                    | `0xde79ed54` | `CrossStateEthCall` | Lamports-only probe — skips data fetch                                                                                                                                                                                                |
| `pdas_batch_derive(bytes[][],bytes32)`                         | `0x944336f8` | `CrossStateEthCall` | N independent PDAs against one program in one dispatch                                                                                                                                                                                |

## HelperProgram (`0xff00000000000000000000000000000000000009`)

Rome user-facing helpers for ATA/PDA creation, SPL transfers/approve/mint signed by the caller's `EXTERNAL_AUTHORITY` PDA, gas↔lamports conversion, ATA-deposit, and SPL\_ERC20-facing reads (`user_balance` / `allowance_of`). Every method is `not_payable`; mutation routes through `Invoke` (single CPI) except `deposit_from_ata` (`Composed`, single-state only), the three read-only `pda`/`ata` queries (`EthCall`), and the two cross-state SPL\_ERC20 reads (`CrossStateEthCall` — one Solana account fetch).

| Solidity signature                                               | Selector     | Dispatch            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------------------------------------------- | ------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create_ata(address)`                                            | `0x5a7c3259` | `Invoke`            | Create ATA for the EVM user under the chain's gas mint                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `create_ata(address,bytes32)`                                    | `0x3de2251a` | `Invoke`            | Create ATA for the EVM user under an arbitrary mint                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `create_ata_for_key(bytes32,bytes32)`                            | `0xd258a69d` | `Invoke`            | Idempotent ATA-create for an arbitrary raw Solana pubkey as owner (not derived via `external_auth`). Operator pays rent. Used by `SPL_ERC20.bridgeOutToSolana` / `ensureRecipientAta` / `create_token_account` raw-pubkey-recipient flows                                                                                                                                                                                                                                                                                                                                               |
| `create_pda(address)`                                            | `0xff3556ca` | `Invoke`            | Create the user's `EXTERNAL_AUTHORITY` PDA (rent-exempt)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `create_pda(address,uint64)`                                     | `0x58e88298` | `Invoke`            | Same, but funded with `lamports` over rent floor                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `swap_gas_to_lamports(uint64)`                                   | `0x6e3f24e0` | `Invoke`            | Credit the caller's PDA with `n` lamports from the transaction's Solana fee payer; the caller pays for them in Rome gas (the payer's lamport spend inside any legacy CPI is added to the transaction fee)                                                                                                                                                                                                                                                                                                                                                                               |
| `transfer_lamports(address,uint64)`                              | `0x5fe71665` | `Invoke`            | Move lamports from caller's PDA to another EVM user's PDA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `transfer_spl(address,uint64)`                                   | `0xb12be5ba` | `Invoke`            | Send chain-gas SPL from caller PDA's ATA to dest user's ATA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `transfer_spl(bytes32,uint64)`                                   | `0xba3a5eac` | `Invoke`            | Send chain-gas SPL from caller PDA's ATA to an explicit ATA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `transfer_spl(address,uint64,bytes32)`                           | `0x53b505e0` | `Invoke`            | Send arbitrary-mint SPL from caller PDA's ATA to dest user's ATA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `transfer_spl(bytes32,uint64,bytes32)`                           | `0xb6977879` | `Invoke`            | Send arbitrary-mint SPL from caller PDA's ATA to explicit ATA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `transfer_spl(bytes32,bytes32,uint64,bytes32)`                   | `0x766b362a` | `Invoke`            | Delegate variant — caller supplies `src_ata` (must be PDA-owned OR PDA must be the delegate with sufficient `delegated_amount`)                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `transfer_spl(address,address,uint64,bytes32)`                   | `0xe479df56` | `Invoke`            | Addr-keyed delegate variant — derives both ATAs from EVM addresses internally. Signs as `external_auth(caller)`; SPL Token accepts PDA as owner OR delegate. Used by `SPL_ERC20.transferFrom`                                                                                                                                                                                                                                                                                                                                                                                           |
| `transfer_spl_to_signer(uint64,bytes32)`                         | `0x46efa679` | `Invoke`            | Solana-native exit leg. Sends `amount` of `mint` from caller PDA's ATA to the **outer Solana signer's** ATA for that mint (the user's own wallet only on the wallet-signed Solana lane; from MetaMask or through the relay, the signer is Rome's proxy payer; no EVM-address conversion). No approve needed — caller-PDA owns the source ATA. Pair it with `ActivateAta` (slot 18) for end-to-end Solana-key-only EVM use. It reaches the wallet on the current v1 lane and on the older legacy `DoTxUnsigned` (slot 17) lane, because the wallet signs the Solana transaction on both. |
| `approve_spl(address,uint64,bytes32)`                            | `0xabf6f675` | `Invoke`            | Caller-PDA-as-owner sets `delegate_pda(spender)` on owner-ATA via SPL `approve_checked`. Used by `SPL_ERC20.approve`                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `approve_spl_raw_delegate(bytes32,bytes32,uint64,bytes32,uint8)` | `0x7881d453` | `Invoke`            | SPL `approve_checked` with caller-supplied raw-pubkey delegate (e.g. Wormhole `authority_signer` PDA, no EVM-address equivalent). Caller passes `decimals` to skip on-chain mint read (\~30-50K CU saving). Hardcoded `spl_program = SPL Token`. Used by `RomeBridgeWithdraw.approveBurnETH`                                                                                                                                                                                                                                                                                            |
| `mint_spl(address,uint64,bytes32)`                               | `0xd795522b` | `Invoke`            | Caller-PDA-as-mint-authority mints to dest user's ATA via SPL `mint_to_checked`. SPL runtime enforces authority match. Used by `SPL_ERC20.mint_to`                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `create_mint_account(bytes32)`                                   | `0xe97d3291` | `Invoke`            | System CreateAccount allocating a salt-derived PDA (`external_auth_with_salt(caller, salt)`) of space=82 owned by SPL Token. Funder = caller's external\_auth PDA. Two PDA signers. Used by `ERC20SPLFactory.create_token_mint`                                                                                                                                                                                                                                                                                                                                                         |
| `init_spl_mint(bytes32,uint8,bytes32,bool,bytes32)`              | `0x4f75e987` | `Invoke`            | SPL Token `InitializeMint2` against pre-allocated mint. No PDA signer (mint\_authority/freeze\_authority stored as data, not runtime signers). Used by `ERC20SPLFactory.init_token_mint`                                                                                                                                                                                                                                                                                                                                                                                                |
| `create_and_init_mint(uint8,bytes32,bool,bytes32,bytes32)`       | `0x20972d0f` | `Composed`          | Combined: System CreateAccount + SPL InitializeMint2 in one dispatch. Saves 1 Rome DoTx envelope vs separate `create_mint_account` + `init_spl_mint` calls                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `deposit_from_ata(uint256)`                                      | `0x4479b709` | `Composed`          | Pull SPL from caller's ATA into Rome native gas. **Single-state only** — rejected on op-geth chains                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `pda(address)`                                                   | `0x8854a299` | `EthCall`           | Derive the user's `EXTERNAL_AUTHORITY` PDA — read-only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `pda_with_salt(address,bytes32)`                                 | `0x5c6d04b3` | `EthCall`           | Salt-derived `EXTERNAL_AUTHORITY` PDA in a single dispatch. Collapses Solidity-side 2-call `rome_evm_program_id() + find_program_address()` composition. Used by `RomeEVMAccount.pda_with_salt`, `RomeBridgeWithdraw.{burnUSDC,burnETH}`                                                                                                                                                                                                                                                                                                                                                |
| `ata(address)`                                                   | `0x31db4f82` | `EthCall`           | Derive `getATA(user_pda, chain_mint_id)` — composes EXT\_AUTH + ATA — read-only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `ata(address,bytes32)`                                           | `0xfeb1c647` | `EthCall`           | Same with an explicit mint — read-only                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `user_balance(address,bytes32)`                                  | `0xdd0119c8` | `CrossStateEthCall` | Read user-ATA `amount` (SPL TokenAccount offset 64). Returns 0 if ATA doesn't exist (fresh-chain probe). Collapses 3 v1 dispatches. Used by `SPL_ERC20.balanceOf`                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `mint_info(bytes32)`                                             | `0xe24bf5d4` | `CrossStateEthCall` | Mint facts for a legacy-track caller: token program, decimals, **armed** hook program (0 when absent *or* inert), current-epoch fee bps, extension-presence bitmap. Mirrored on `SplCached` at the same selector. `feeBps` is a **predicate** — it says a fee is armed, it is not an operand, since the real fee is capped by `maximum_fee` which this deliberately does not carry.                                                                                                                                                                                                     |
| `allowance_of(address,address,bytes32)`                          | `0xed72dbc8` | `CrossStateEthCall` | Read owner-ATA delegated\_amount with **delegate-equality check** — returns 0 unless on-chain delegate == `external_auth(spender)`. ERC-20 semantic-correct (HARD REQ). Used by `SPL_ERC20.allowance`                                                                                                                                                                                                                                                                                                                                                                                   |

## Withdraw (`0x4200000000000000000000000000000000000016`)

Rome → Solana native gas withdrawal.

| Solidity signature         | Selector     | Dispatch               | Description                                                                                                  |
| -------------------------- | ------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `withdrawal(bytes32)`      | `0x4d8b0ea4` | `Composed` (`payable`) | Burn `msg.value` Rome gas; mint+transfer to a Solana system-account `to`. The only payable precompile method |
| `withdraw_to_pda(uint256)` | `0x7f3124a0` | `Composed`             | Burn gas; deposit to caller's `EXTERNAL_AUTHORITY` PDA. **Single-state only**                                |
| `withdraw_to_ata(uint256)` | `0x8059abc0` | `Composed`             | Burn gas; deposit to `getATA(caller_pda, chain_mint_id)`. **Single-state only**                              |


# Contract Addresses

Precompiles, program IDs, and per-chain deployed contract addresses.

Precompile addresses are built into the Rome EVM program and are identical on every chain. Per-chain deployed contracts are **canonical in the** [**Rome registry**](https://github.com/rome-protocol/rome-registry/tree/main/chains). The Hadrian table below lists the headline contracts that the registry (release v0.2.0) marks `live`; the registry is the single source of truth, and a contract can be redeployed after this page was written. See [Reading the contracts list](#reading-the-contracts-list) for how to pick the live address in code.

## Precompiles (all chains)

Standard Ethereum precompiles, with mainnet-equivalent semantics:

| Precompile    | Address                                      |
| ------------- | -------------------------------------------- |
| ecrecover     | `0x0000000000000000000000000000000000000001` |
| SHA-256       | `0x0000000000000000000000000000000000000002` |
| RIPEMD-160    | `0x0000000000000000000000000000000000000003` |
| identity      | `0x0000000000000000000000000000000000000004` |
| modexp        | `0x0000000000000000000000000000000000000005` |
| BN254 add     | `0x0000000000000000000000000000000000000006` |
| BN254 mul     | `0x0000000000000000000000000000000000000007` |
| BN254 pairing | `0x0000000000000000000000000000000000000008` |
| blake2f       | `0x0000000000000000000000000000000000000009` |

Rome non-EVM precompiles, for reaching into Solana (interfaces in [`rome-solidity/contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol)):

| Precompile     | Address                                      | Interface                 | Purpose                                       |
| -------------- | -------------------------------------------- | ------------------------- | --------------------------------------------- |
| System         | `0xFF00000000000000000000000000000000000007` | `ISystemProgram`          | PDA derivation, base58 conversion             |
| CpiProgram     | `0xFF00000000000000000000000000000000000008` | `ICrossProgramInvocation` | Arbitrary CPI + cross-state reads             |
| HelperProgram  | `0xFF00000000000000000000000000000000000009` | `IHelperProgram`          | ATA/PDA creation, SPL transfers, gas↔lamports |
| Withdraw       | `0x4200000000000000000000000000000000000016` | `IWithdraw`               | Withdraw SOL/SPL from EVM to Solana           |
| SystemCached   | `0xFF00000000000000000000000000000000000004` | `ISystemCached`           | Cached-track System helpers                   |
| SplCached      | `0xFF00000000000000000000000000000000000005` | `ISplCached`              | Cached-track SPL operations                   |
| ASplCached     | `0xFF00000000000000000000000000000000000006` | `IAssociatedSplCached`    | Cached-track ATA operations                   |
| WithdrawCached | `0xFF0000000000000000000000000000000000000b` | `IWithdrawCached`         | Cached-track withdraw                         |

A contract uses one track — legacy (`SplCached` off) or cached — consistently; the two are not mixed within a call.

## Program IDs

rome-evm programs (the Solana program that runs the EVM), per Rome network:

| Network           | Program ID                                    |
| ----------------- | --------------------------------------------- |
| Hadrian (testnet) | `RPTWwELXAY4KC9ZPHhaxp7Sq1hHtU3HNEgLbSegCcWf` |
| Rubicon (mainnet) | `RomePq9X3iAoTr813HR7uRarafFDUJty6GdiEceYpzX` |

Well-known Solana programs (same as anywhere on Solana):

| Program                 | ID                                             |
| ----------------------- | ---------------------------------------------- |
| SPL Token               | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`  |
| SPL Token-2022          | `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`  |
| Associated Token        | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` |
| System Program          | `11111111111111111111111111111111`             |
| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`  |

## Rome ZK EVM on Solana devnet

These v0.3.0 programs are shared by operator-run ZK EVM chains on Solana devnet. The [tagged program list](https://github.com/rome-protocol/solana-zk-evm/blob/v0.3.0/deploy/rollup/programs.devnet.json) is the source for these IDs.

| Program       | ID                                             |
| ------------- | ---------------------------------------------- |
| zk-inbox      | `28948c2Qt1QtG2XJA823ytzCfj7U5cNNm3FuM2KoXsAa` |
| zk-settlement | `8anSjJZu5vgfNbESPLoKudVBNEraDZASwKJnkDLTCaGo` |
| veritas       | `2cLGd9FKC7TiZrEHCvpw291AwXNcGgP9nDT4W3PHLe5k` |
| zk-bridge     | `27TbMDUyVynpFpqeKygpUMcDzWKHfW4k9aRN5yCysLEQ` |

Node image: `ghcr.io/rome-protocol/rome-zk-evm:v0.3.0`.

## Deployed contracts — Hadrian (testnet, 200010)

Headline contracts, taken from the `live` entries of the [registry](https://github.com/rome-protocol/rome-registry/tree/main/chains/200010-hadrian) (v0.2.0). The full set (SPL wrappers, Uniswap V2/V3/V4, Aave, oracle feeds) is in the registry.

| Contract             | Address                                      |
| -------------------- | -------------------------------------------- |
| ERC20SPLFactory      | `0x37c13e4ff21fb914eb24014dcbd0f8dbeaa31078` |
| Multicall3           | `0xb7180d3c46632b582b99d9af3daae394fab9ae4c` |
| OracleAdapterFactory | `0x52b008771fc95b4674a4f3533570b3d223f27f68` |
| BatchReader          | `0x2869f0a96572e7fb15036656e9a32fd17346499b` |
| RomeBridgeWithdraw   | `0x6eeacb4e6006b7a29c8141f69b6c75346dedec73` |
| MeteoraDAMMv1Factory | `0xd68b355f62643de0ec40243cba8f699f959ea3c4` |
| UniswapV3Factory     | `0xD9E323088561D40267767b52252595ea9cF1D5bD` |
| UniswapV4PoolManager | `0x8a281Be9B5173B2F52328437eAa0d272791707f7` |

## Deployed contracts — Rubicon (mainnet, 7531)

Headline contracts (the public registry publishes the Hadrian set today; the Rubicon set is listed here). Wrappers: `wUSDC` `0x0952d233b0c254cd375f3bfb3e5869c1a906f9b3`, `wETH` `0x01e8b3573da1d4b7b4b1ce0932cf07c8af0c92e9`, `wSOL` `0xc4e29e0f726a906e93277cbbc37bf27a9848a113`.

| Contract                        | Address                                      |
| ------------------------------- | -------------------------------------------- |
| ERC20SPLFactory                 | `0x4068aa7482dc15e9f78e22fbe0f884531c83ae6e` |
| Multicall3                      | `0xd6e0b13ea70aea0d44db083544f01d8b7693a759` |
| OracleAdapterFactory            | `0x6a1340dd86c3951b6f13d9001b3951d56a2fe581` |
| BatchReader                     | `0x580dc0ecb226530ee8a90b57e0f0cd2555fb4f0f` |
| RomeBridgeWithdraw              | `0xceea191761cdba2fc589996d7dd096e57db29c84` |
| WETH9                           | `0x8dc8bf908efa4511f71a9e2ed95f46a70507f1ae` |
| UniswapV2Factory                | `0x23af4d2065d24b42a1a17f22b963c9216d0a1dc2` |
| UniswapV2Router                 | `0x445d7af7888db44a4904b63336cc43446dfb37f7` |
| UniswapV3Factory                | `0x02aed7762fdc8bae826e7ee0ac2d2e1f754ba025` |
| SwapRouter (V3)                 | `0x680b7645c63421e33584a02aca91be6642d3d034` |
| NonfungiblePositionManager (V3) | `0xfb132adf6b7d811408f3639e91c8f638bddd6123` |
| QuoterV2 (V3)                   | `0x1d68d09dcc3a8c1b9aa8f0e2b138205c04bad4a7` |

## Reading the contracts list

Each chain folder in the registry has a `contracts.json`. It is a list of contracts. Every entry has a `name` and a `versions[]` list, and each version has an `address`, a `version` and a `status` of `live`, `deprecated` or `retired`. A contract that was redeployed keeps its old versions in the list, so the first or the last item is not necessarily the one to use. Pick the version whose `status` is `"live"`. A few entries (Hadrian's Aave contracts) have more than one live version, so filter on `status` and choose on purpose rather than taking the first match.

```ts
import { getContracts } from "@rome-protocol/registry";
const factory = getContracts(200010).find((c) => c.name === "ERC20SPLFactory");
const address = factory.versions.find((v) => v.status === "live").address;
```

`getContracts(chainId)` returns the list for that chain, or `null` if the chain is not published. Some entries have no live version at all (Hadrian's `RomeswapMinimalRouter` is one), so check that the `find` found something before reading `.address`. The registry package is installed from GitHub, not npm: `npm install github:rome-protocol/rome-registry#v0.2.0`.

## Environments

| Environment       | Chain ID | RPC                                | Status |
| ----------------- | -------- | ---------------------------------- | ------ |
| Rubicon (mainnet) | `7531`   | `rubicon.mainnet.romeprotocol.xyz` | Live   |
| Hadrian (testnet) | `200010` | `hadrian.testnet.romeprotocol.xyz` | Live   |


# Known Limitations

Current Rome EVM constraints and their implications — unsupported opcodes, storage account layout, CU ceilings, CPI depth, and oracle limits.

Honest documentation of Rome Protocol's current constraints and their implications.

## Execution Limitations

**SELFDESTRUCT not supported.** The opcode has no handler, so a contract that executes it aborts the whole transaction (see below), it does not revert a single frame.

**An opcode Rome does not implement aborts the whole transaction.** The interpreter has no handler for the byte, so the transaction ends with `IncompatibleVersionEVM(opcode)`, nothing it did is committed, and the failure reaches the sender instead of being swallowed by a calling contract.

**No per-opcode gas schedule.** The gas value passed to `CALL`, `DELEGATECALL`, `STATICCALL` and `CREATE` is accepted for ABI compatibility but not enforced — a callee runs with the caller's full remaining budget, and there is no 2300-gas stipend. Stipend-based reentrancy protection (`transfer()` / `send()`) is void on Rome; use explicit reentrancy guards and checks-effects-interactions.

**Modexp (0x05) is disabled.** A call reverts with `big_mod_exp call is disabled` until Solana activates the `big_mod_exp` syscall.

**256 storage slots per storage account.** Contract storage is partitioned across Solana accounts with 256 slots each. Contracts with large storage footprints use multiple accounts, which affects CU costs.

**\~1.4M compute units per atomic transaction.** Work that exceeds a single Solana transaction's budget falls back to iterative execution (split across multiple transactions). Some operations are atomic-only and can't run iteratively — notably legacy-track CPI (`CpiProhibitedInIterativeTx`; cached-track effects are staged and do run iteratively).

**CPI depth limit of 4.** Deeply nested CPI calls will fail. Design contracts with shallow call trees.

## Oracle Limitations

**No historical round data.** Oracle Gateway adapters only support `latestRoundData()`. Historical price queries via `getRoundData(roundId)` revert.

**Parser offsets are validated against current layouts.** Pyth and Switchboard account data is parsed using byte offsets. If Pyth or Switchboard change their account layout, adapters return incorrect data until offsets are re-validated.

## Tooling Limitations

**No Ethereum-style transaction tracing.** `debug_traceTransaction` and `trace_*` are not supported. The one trace method is `debug_traceRomeTransaction`, which breaks a transaction that bundles several calls into one batch into its sub-calls and CPIs. Ordinary transactions return `null`. For everything else, use `eth_call` and the [Via explorer](/apps-on-rome/via) (which shows the Solana instructions behind a transaction) to debug. See [JSON-RPC](/reference/json-rpc).

**`eth_getLogs` is range-capped.** Public chains reject overly wide block ranges (error `-32005`); bound your queries. See [JSON-RPC](/reference/json-rpc).

## Infrastructure Limitations

**Single operator model.** Each Rome deployment is operated by a single entity (the payer pool operator). There is no decentralized operator set.

**Iterative mode locking.** During iterative execution, accounts are locked for a few seconds, which can cause contention on heavily-used accounts.

## What's Next

* [Responsible Disclosure](/security/responsible-disclosure) — how to report issues


# Responsible Disclosure

How to report a security vulnerability in Rome Protocol responsibly — contact channels, what to include, and disclosure guidelines.

If you discover a security vulnerability in Rome Protocol, please report it responsibly.

## Reporting

Email <rome@romeprotocol.com>, or open a ticket in the [Rome Discord](https://discord.gg/vZ9rnCdNSB) to reach the core team privately.

**What to include:**

* Description of the vulnerability
* Steps to reproduce
* Potential impact assessment
* Your contact information for follow-up

## Guidelines

* Do **not** publicly disclose the vulnerability before a fix is deployed
* Do **not** exploit the vulnerability beyond what is necessary to demonstrate it
* Do **not** access or modify data belonging to other users
* Allow reasonable time for the team to investigate and fix the issue

## Scope

In scope:

* Rome EVM on-chain program
* Rome Proxy and Hercules services
* Rome Solidity SDK and contract libraries
* Oracle Gateway adapters
* Bridge contracts (ERC20SPL, Factory)

Out of scope:

* Third-party dependencies (report to their maintainers)
* Social engineering attacks
* Denial of service against testnet/devnet infrastructure
* Issues in deprecated or archived repositories

## Community

* [Discord](https://discord.gg/vZ9rnCdNSB) — general questions and support
* [Telegram](https://t.me/+tdnr-M6kcngxYzhk) — community updates
* [GitHub](https://github.com/rome-protocol) — code and issue tracking


# DeFi Protocols

Integration patterns for EVM DeFi on Rome — lending with Solana oracles, composing Jupiter and Kamino from Solidity, and shared-liquidity pools.

Rome enables EVM DeFi protocols to compose with Solana's native DeFi ecosystem atomically. This page covers common integration patterns. Several are already live on Rome — Aave v3, Compound v3 (Aerarium), Uniswap V2/V3/V4, and Rome DEX — see [Apps on Rome](/apps-on-rome/apps).

The code below is illustrative; interface names for third-party Solana programs are examples, not shipped SDK interfaces.

## Why DeFi on Rome?

* **Access Solana liquidity** — Jupiter, Kamino, Drift, Meteora, Raydium, Orca
* **Atomic composability** — multi-step DeFi operations in a single transaction
* **Solidity tooling** — familiar development and audit ecosystem
* **Shared state** — EVM and Solana users share the same pools

## Pattern 1: Lending Protocol with Solana Oracles

The import below comes from [`rome-solidity`](https://github.com/rome-protocol/rome-solidity), which is installed from GitHub, not from npm: `npm install github:rome-protocol/rome-solidity`, or `forge install rome-protocol/rome-solidity` with the remapping `@rome-protocol/rome-solidity/=lib/rome-solidity/` (details in [Deploy Solidity Contracts](/developer-guides/deploy-solidity#using-the-rome-solidity-sdk)). Pass the feed address in from the [registry](/products/oracle-gateway#consume-a-feed-in-three-steps) rather than hardcoding it.

```solidity
import {IAggregatorV3Interface} from "@rome-protocol/rome-solidity/contracts/oracle/IAggregatorV3Interface.sol";

contract RomeLending {
    IAggregatorV3Interface public priceFeed;

    constructor(address _priceFeed) {
        priceFeed = IAggregatorV3Interface(_priceFeed);
    }

    function getCollateralValue(uint256 amount) public view returns (uint256) {
        (, int256 price,,,) = priceFeed.latestRoundData();
        // Pyth/Switchboard price via Oracle Gateway
        // Same interface as Chainlink on Ethereum
        return (amount * uint256(price)) / 1e8;
    }
}
```

## Pattern 2: DEX Aggregator via CPI

```solidity
contract RomeSwap {
    function swap(
        bytes32 fromMint,
        bytes32 toMint,
        uint256 amount,
        uint256 minOut
    ) external {
        // CPI to Jupiter/Raydium/Meteora
        // All Solana DEX liquidity accessible from Solidity
        CpiProgram.invoke(JUPITER_PROGRAM, accounts, swapData);
    }
}
```

## Pattern 3: Yield Vault

```solidity
contract YieldVault {
    function deposit(uint256 amount) external {
        // Accept USDC deposit
        // CPI → Jupiter: swap to optimal tokens
        // CPI → Kamino: supply as collateral
        // CPI → Drift: open delta-neutral hedge
        // All atomic — one tx, all-or-nothing
    }
}
```

## Pattern 4: Cross-Protocol Arbitrage

Using `RemusTx` (atomic cross-rollup transactions):

1. Buy on DEX A in rollup 1
2. Sell on DEX B in rollup 2
3. Both atomically — zero execution risk

## Reference implementations

Fork a working Rome app instead of starting from a blank page:

* [**rome-dex**](https://github.com/rome-protocol/rome-dex) — dual-lane AMM; bring a new idea and deploy it for both Solana and EVM users over one pool.
* [**aerarium**](https://github.com/rome-protocol/aerarium) — dual-lane Compound v3 lending UI.
* [**cardo**](https://github.com/rome-protocol/cardo) — CPI app-distribution portal (swap / lend / perps / compose).
* [**rome-aave-v3-demo**](https://github.com/rome-protocol/rome-aave-v3-demo) — Aave v3 lending UI.
* [**compound-on-rome-comet**](https://github.com/rome-protocol/compound-on-rome-comet) — the Comet money-market contracts.
* [**rome-oracle-gateway**](https://github.com/rome-protocol/rome-oracle-gateway) — a greenfield cross-VM app: reads Solana price accounts (Pyth, Switchboard), posts them on EVM, and serves them to Solidity through the standard Chainlink `AggregatorV3Interface`.

## Related

* [Oracle Gateway](/products/oracle-gateway) — Chainlink-compatible price feeds
* [Rome SDK](/products/rome-sdk) — typed interfaces for DeFi protocols


# Faucets

Test-token faucets for Rome and every chain the bridge supports.

Rome's test chains use test USDC; **Rubicon mainnet uses real USDC** — bring it in with the [Rome Bridge](https://bridge-api.mainnet.romeprotocol.xyz/) (see [Rubicon](/networks/rubicon)), there is no faucet. The faucets below are for the test chains.

## Fund a Rome wallet

Every Rome chain's gas token is **USDC** (Circle) — there is **no Rome faucet**. You fund a wallet by **bridging USDC into Rome** from a chain where you can get test USDC: an EVM testnet or Solana Devnet. That's the one manual step; after it, deploy and transact normally. Get the USDC and bridge it in using the sources below.

## From Solana Devnet

A tester who works from a Solana wallet needs two things on Solana Devnet, both from faucets:

* **Devnet USDC** for Hadrian: the [Circle faucet](https://faucet.circle.com/), with the network set to Solana Devnet. Hadrian uses the mint `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`, so this is the same USDC the chain uses as its gas token.
* **Devnet SOL** for Solana transaction fees: the [Solana faucet](https://faucet.solana.com/). SOL is not the gas token; Rome chains charge gas in USDC.

What you do next depends on what you are testing.

* **Calling contracts from a Solana wallet (the Solana lane).** The USDC and SOL stay in that Solana wallet. The app's fund step moves the USDC into the call when it is needed, so there is nothing to bridge. See [Open your contract to Solana users](/developer-guides/open-to-solana-users).
* **Funding an EVM wallet from Solana.** Bridge the USDC with the [Rome Bridge](/apps-on-rome/bridge-api) using its Solana route, with the EVM wallet's address as the recipient. The bridge then asks that EVM wallet to sign a claim transaction on Rome, which turns the USDC into gas. The live routes are at [`/v1/routes`](https://bridge-api.devnet.romeprotocol.xyz/v1/routes).

## Bridge in from another chain

The [Rome Bridge](/apps-on-rome/bridge-api) moves **USDC** (via CCTP) and **ETH** (via Wormhole) into Rome from seven EVM source chains, and USDC and SOL from Solana Devnet (above). Fund the source chain first, then bridge.

### USDC — all source chains

The [Circle faucet](https://faucet.circle.com/) mints test USDC on every CCTP source chain — select the chain in the faucet.

### Native gas — per source chain

You also need the source chain's native token to pay its own bridge transaction:

| Source chain     | Chain ID | Native | Faucet                                                                                   |
| ---------------- | -------- | ------ | ---------------------------------------------------------------------------------------- |
| Ethereum Sepolia | 11155111 | ETH    | [Google Cloud faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) |
| Arbitrum Sepolia | 421614   | ETH    | [Alchemy faucet](https://www.alchemy.com/faucets/arbitrum-sepolia)                       |
| Base Sepolia     | 84532    | ETH    | [Alchemy faucet](https://www.alchemy.com/faucets/base-sepolia)                           |
| Polygon Amoy     | 80002    | POL    | [Polygon faucet](https://faucet.polygon.technology/)                                     |
| Avalanche Fuji   | 43113    | AVAX   | [Avalanche faucet](https://faucet.avax.network/)                                         |
| Monad Testnet    | 10143    | MON    | [Monad faucet](https://faucet.monad.xyz/)                                                |
| OP Sepolia       | 11155420 | ETH    | [Alchemy faucet](https://www.alchemy.com/faucets/optimism-sepolia)                       |

ETH bridged via Wormhole comes from Ethereum Sepolia (use the Sepolia ETH faucet above). These are third-party faucets and may rate-limit or change.

## What's Next

* [Networks](/networks/networks) — connect to Rubicon or Hadrian
* [Rome Bridge](/apps-on-rome/bridge-api) — the bridge and its API


# FAQ

Frequently asked questions about Rome Protocol — is Rome a bridge or a rollup, Solidity compatibility, oracles, gas tokens, and Solana interop.

## General

**What is Rome Protocol?** Rome is an EVM execution environment running natively inside the Solana runtime. Solidity contracts deploy on Solana with direct CPI access to all Solana programs. See [What is Rome?](/getting-started/what-is-rome)

**Is Rome a bridge?** No. Rome EVM runs inside Solana's runtime as a first-class program. There's no message relay between separate chains. EVM state IS Solana state.

**Is Rome a rollup?** Not the Rome EVM chains (Rubicon and Hadrian). A block indexer (Hercules) produces Ethereum-compatible blocks for wallets and explorers, but EVM execution happens inside a Solana program — not on a separate rollup chain. The [Rome ZK EVM](/rome-zk-evm/zk-evm) is a different product: operators run rollups that post batches to Solana and settle proved roots there.

## Development

**Can I use my existing Solidity contracts?** Yes. Standard Solidity contracts deploy unchanged on Rome. If your contracts use Chainlink oracles, the Oracle Gateway provides the same `AggregatorV3Interface`. One thing is new for users, not for the contract: before a wrapper can move a wallet's tokens, that wallet grants the wrapper once (`approve_spl` on the Helper precompile). A contract that pulls a wrapper token with `transferFrom` needs each user to have done this. See [One-time grant per wallet](/core-concepts/token-interop#one-time-grant-per-wallet).

**What Solidity version should I use?** 0.8.28 is recommended (matches the Rome Solidity SDK). Earlier versions work.

**Can I use Hardhat? Foundry?** Both work. Configure your network with the Rome RPC URL and chain ID. See [Deploy Solidity](/developer-guides/deploy-solidity).

**Can I use MetaMask?** Yes. Add Rome as a custom network in MetaMask with the appropriate RPC URL and chain ID. See [Quickstart](/getting-started/quickstart).

**How much gas does a transaction use?** Ask the chain: use `eth_estimateGas`, as standard tooling does, and never hardcode 21,000. Most of a write's cost is creating Solana accounts, such as a never-used recipient, a new token account or a deployed contract, and the estimate includes it. Measured on Hadrian, a native transfer to a never-used address estimated 1,086,961 gas, while a transfer to an existing address estimated 21,000 and used 10,001. Rome charges the gas a transaction actually uses, which in these measurements was never more than the estimate. For calls into Solana programs, add a margin, as the SDK's 1.3x pad does, and set a gas ceiling if the estimate itself reverts. See [JSON-RPC Support](/reference/json-rpc).

**How do I call Solana programs from Solidity?** Use the CPI precompile (`0xFF...08`) via the Rome Solidity SDK. See [Call Solana from EVM](/developer-guides/call-solana-from-evm).

## Performance

**What's the transaction finality time?** A transaction is confirmed within about one Solana slot, a fraction of a second. That is confirmation on the Rome chain, not finality on Solana, which takes longer.

**What's the compute budget?** \~1.4M compute units per atomic transaction. Operations exceeding this use iterative mode (split across multiple Solana transactions). See [Compute Budget](/core-concepts/compute-budget).

## Tokens

**What gas token does Rome use?** Each chain chooses its own gas token — any SPL token, priced via Meteora pools. The public chains (Rubicon and Hadrian) use USDC as gas. On Hadrian it is test USDC; on [Rubicon](/networks/rubicon) it is real USDC.

**How do ERC-20 tokens work on Rome?** ERC-20 tokens on Rome are wrappers over SPL tokens. For a wallet address, `balanceOf()` reads the SPL token account on Solana. For a contract address, the wrapper keeps the balance in its own ledger, because the contract's tokens sit in the wrapper's token account. `allowance()` is kept in EVM storage, apart from Solana delegates, and the wrapper moves a user's tokens as that user's SPL delegate after the one-time grant. See [Token Interop](/core-concepts/token-interop).

**A Solana-side deposit or bridge transfer fails with "insufficient SOL," but my wallet is funded — why?** Rome's public testnet, Hadrian, settles on Solana Devnet, so your Solana wallet must be in the matching network mode (Rubicon settles on Solana mainnet). In Phantom, open Settings → Developer Settings → enable Testnet Mode with the network set to Devnet, then disconnect and reconnect both wallets and retry. If a token balance doesn't appear after a deposit, import the token into your wallet manually.

## Infrastructure

**Do I need to run my own node?** No — connect to a public chain's RPC (see [Networks](/networks/networks)). Running your own chain means operating the Proxy and Hercules.

**What databases does Rome need?** PostgreSQL, used by Hercules for block indexing and by the Proxy for block queries.

**Can I run Rome locally?** Not as a ready-made local stack; there is no public one. Develop against a public chain — the [Quickstart](/getting-started/quickstart) deploys to Hadrian (testnet) in minutes.


# Glossary

Definitions of Rome and Solana terms used across these docs — ATA, CPI, PDA, VmAt/VmIt, holder accounts, SPL tokens, and more.

Rome-specific and Solana terms used throughout this documentation.

## A

**Allocation** — A Canton token set aside for a receiver and executor. In Ostia, `Advance` executes the allocation for a deposit or payment. The sender can take it back before settlement. See [Deposits, withdrawals and payments](/canton-zk-evm/deposits-withdrawals-payments).

**ATA (Associated Token Account)** — A deterministic token account for a given wallet + mint pair on Solana. Derived from the wallet pubkey, token mint, and ATA program.

**Atomic Execution (VmAt)** — EVM transaction mode where the entire execution completes within a single Solana transaction. Default mode for most operations.

## B

**Batch guest** — The ZisK program built for a chain's genesis. It checks a batch's inbox data and execution, then commits the public values that Solana settlement checks.

**BN254** — An elliptic curve used for ZK proof verification. Rome supports ecAdd (0x06), ecMul (0x07), and ecPairing (0x08) precompiles.

**Borsh** — Binary Object Representation Serializer for Hashing. Solana's standard serialization format. Little-endian byte ordering.

## C

**Canton** — Orders Ostia's blocks and commits each proven EVM block with its Canton actions in one transaction. See [Canton ZK EVM](/canton-zk-evm/canton-zk-evm).

**Chain ID** — A unique identifier for each EVM environment on Rome. Each application gets its own chain ID.

**CPI (Cross-Program Invocation)** — One Solana program calling another within the same transaction. How Rome EVM contracts interact with Solana programs.

**Compute Units (CU)** — Solana's equivalent of Ethereum gas. Each transaction has a compute budget; operations consume CU.

**Confirmer** — A signer of Ostia's chain record. Its participant runs a sidecar that checks block proofs and gateway legs through Canton's external call. See [Trust model and limits](/canton-zk-evm/trust-model).

## D

**Daml** — Used for Ostia's contracts on Canton: the chain's head, block records and the contracts that settle deposits, withdrawals and payments. See [How it works](/canton-zk-evm/how-it-works).

## E

**ERC20SPL** — An ERC-20 wrapper contract that reads balances from the underlying SPL token account on Solana. For a wallet the ERC-20 balance is the SPL balance; tokens held by a contract sit in the wrapper's escrow account and are tracked in its storage.

**ERC20SPLFactory** — A factory contract that deploys ERC20SPL wrappers for any SPL token mint.

**Exit prover** — The service that proves a withdrawal against a final root on Solana, then sends its vault payout.

**External call** — The Canton extension through which Daml calls each confirming participant's sidecar. Ostia uses it for the `verify` and `legs` checks. See [How it works](/canton-zk-evm/how-it-works).

## G

**Gateway** — In Ostia, the EVM contract that records a block's Canton legs and creates wrapped tokens. The separate gateway party on Canton holds the tokens that back the wrapped tokens on the EVM. See [Deposits, withdrawals and payments](/canton-zk-evm/deposits-withdrawals-payments).

## H

**Hercules** — The block indexer service. Monitors Rome EVM events on Solana and produces Ethereum-compatible block data.

**Holder Account** — An on-chain buffer (max 80 KB) that stores large EVM transactions exceeding the 4 KB Solana (v1) transaction limit.

## I

**Iterative Execution (VmIt)** — EVM transaction mode that splits execution across multiple Solana transactions, each step packing as many opcodes as fit in its compute budget (adaptive step size). Used for compute-intensive operations.

## L

**Lamports** — Smallest unit of SOL. 1 SOL = 10^9 lamports.

**Leg** — A Canton action recorded by Ostia's EVM gateway: a deposit, withdrawal or payment. The proven block and its legs settle together in `Advance`. See [Deposits, withdrawals and payments](/canton-zk-evm/deposits-withdrawals-payments).

## M

**Mollusk SVM** — Off-chain Solana VM emulator used by Rome Proxy for `eth_call` and gas estimation. Can execute arbitrary BPF programs.

## O

**Oracle Gateway** — Adapter contracts exposing Pyth and Switchboard price feeds through Chainlink's AggregatorV3Interface.

## P

**Participant** — A Canton node that hosts parties. In Ostia's local network, the operator and confirmer participants each run a sidecar, while the users participant hosts the test parties and reader. See [Run it locally](/canton-zk-evm/run-it-locally).

**Party** — A Canton identity hosted on a participant. Ostia's contracts name parties as signers, controllers or observers; the gateway party holds the locked Canton tokens. See [Trust model and limits](/canton-zk-evm/trust-model).

**Payer** — A Solana keypair that signs and pays for Solana transactions on behalf of EVM users. Managed by the Proxy.

**PDA (Program Derived Address)** — A deterministic Solana address derived from seeds and a program ID. No private key — the program "owns" the PDA.

**Proved root** — A state root posted with a ZisK proof checked by Solana settlement. For a permissionless Rome ZK EVM chain, it is final when posted.

**Proxy** — The Rome JSON-RPC server (port 9090) that translates Ethereum API calls into Solana transactions.

## R

**RheaTx** — A single EVM transaction on one rollup.

**RemusTx** — Multiple EVM transactions across rollups, executed atomically.

**RomulusTx** — EVM transactions + native Solana instructions, executed atomically.

**ResourceFactory** — SDK component that pools Solana keypairs and holder account indices for parallel transaction submission.

## S

**Sidecar** — The service beside each confirming participant that checks Ostia's proof, block data and gateway legs when Canton calls it. See [How it works](/canton-zk-evm/how-it-works).

**SPL Token** — Solana's standard token program. All fungible tokens on Solana (USDC, SOL, etc.) are SPL tokens.

**StateHolder** — An on-chain account that stores serialized VM state between iterative execution steps.

**Synchronizer** — Ostia's local Canton network has one synchronizer, made up of a sequencer and a mediator. All three participants connect to it. See [Run it locally](/canton-zk-evm/run-it-locally).

## T

**Token-2022** — Next-generation SPL token program with extensions (Transfer Hooks, Confidential Transfers, Permanent Delegates).

**Transfer Hook** — Token-2022 extension that invokes a program on every `transfer_checked` call.

**TxBuilder** — SDK component that builds Solana instructions from EVM transactions.

## V

**Verification key (programVK)** — The chain-specific guest identifier registered on Solana. Settlement accepts a proof only when its programVK matches an active entry for that chain.

## Z

**ZisK** — The proving system that runs a chain's batch guest and produces the proof checked by Rome ZK EVM settlement.


# Community

Where to get help building on Rome Protocol — Discord, Telegram, GitHub, and Rome Academy courses.

## Channels

| Channel      | Link                                                         | Purpose                                 |
| ------------ | ------------------------------------------------------------ | --------------------------------------- |
| Discord      | [discord.gg/vZ9rnCdNSB](https://discord.gg/vZ9rnCdNSB)       | Developer support, community discussion |
| Telegram     | [t.me/+tdnr-M6kcngxYzhk](https://t.me/+tdnr-M6kcngxYzhk)     | Announcements and updates               |
| GitHub       | [github.com/rome-protocol](https://github.com/rome-protocol) | Source code, issues, contributions      |
| Rome Academy | [schola.rome.builders](https://schola.rome.builders/)        | Courses for building on Rome            |
| Website      | [rome.builders](https://www.rome.builders)                   | Overview and documentation links        |

## Getting Help

1. **Check the docs** — this documentation covers most common questions
2. **Search Discord** — your question may already be answered
3. **Ask in Discord** — the developer community is active and helpful
4. **Open a GitHub issue** — for bugs, feature requests, or documentation improvements

## Contributing

Rome Protocol repositories are on GitHub at [github.com/rome-protocol](https://github.com/rome-protocol). Contributions are welcome via pull requests.


