# Token Interop

Rome EVM bridges ERC-20 tokens and SPL tokens through a single-state model. This page explains how tokens work across EVM and Solana.

## The Single-State Model

Unlike traditional bridges, Rome doesn't lock tokens on one chain and mint wrapped copies on another. Instead, ERC-20 tokens on Rome EVM are **transparent wrappers** over the underlying SPL token accounts on Solana.

```
┌──────────────────────────────────┐
│ Rome EVM                         │
│                                  │
│   ERC-20 "rUSDC"                 │
│   ┌────────────────────────┐     │
│   │ balanceOf(user)        │─────┼──► reads directly from SPL ATA
│   │ transfer(to, amount)   │─────┼──► executes SPL transfer via precompile
│   │ totalSupply()          │─────┼──► reads SPL mint supply
│   └────────────────────────┘     │
│                                  │
└──────────────────────────────────┘
                 │
                 │ same underlying data
                 ↓
┌──────────────────────────────────┐
│ Solana                           │
│                                  │
│   SPL Token Account (ATA)        │
│   Owner: user's PDA              │
│   Mint: USDC (Circle native)     │
│   Amount: 1000000 (= 1 USDC)    │
│                                  │
└──────────────────────────────────┘
```

**What this means:**

* No bridging delay — ERC-20 balance IS the SPL balance
* No liquidity fragmentation — DeFi on both sides sees the same tokens
* No bridge risk — there's no separate escrow to exploit

## ERC20SPL: The Wrapper Contract

`SPL_ERC20` is the standard wrapper contract that provides a full ERC-20 interface over an SPL token mint:

```solidity
import {SPL_ERC20} from "@rome-protocol/solidity-sdk/contracts/token/ERC20SPL.sol";

// The wrapper reads balances from the user's SPL ATA
uint256 balance = wrapper.balanceOf(userAddress);

// Transfers execute via the SPL Token precompile
wrapper.transfer(recipient, amount);
```

**How it works under the hood:**

* `balanceOf()` → derives user's ATA (Associated Token Account) → reads balance from Solana
* `transfer()` → calls SPL Token precompile (`0xff...05`) → moves tokens on Solana
* `approve()` / `allowance()` → uses EVM storage (standard ERC-20 pattern) since SPL doesn't natively support EVM-style allowances
* `totalSupply()` → reads from the SPL mint account

## ERC20SPLFactory

The factory contract deploys wrappers for any SPL token:

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

ERC20SPLFactory factory = ERC20SPLFactory(0xfd21da046c282e1d36cc45e46d9599cff5742f2b);

// Deploy a wrapper for an SPL mint (loads name/symbol from Metaplex metadata)
address wrapper = factory.add_spl_token_with_metadata(splMintPubkey);

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

**Factory address (devnet):** `0xfd21da046c282e1d36cc45e46d9599cff5742f2b`

## Token Registry

The `TokenRegistry` provides admin-controlled registration of approved SPL tokens with cross-chain metadata:

```solidity
import {TokenRegistry, TokenOrigin} from "@rome-protocol/solidity-sdk/contracts/token/TokenRegistry.sol";

// Register a native SPL token
registry.registerToken(
    splMint,
    TokenOrigin.NativeSPL,
    bytes32(0),   // no external address
    0             // no external chain
);

// Register a Wormhole-wrapped token with cross-chain metadata
registry.registerToken(
    wormholeMint,
    TokenOrigin.WormholeWrapped,
    externalTokenAddress,  // original token on source chain
    2                       // Wormhole chain ID for Ethereum
);
```

The registry ensures each asset maps to a single canonical SPL mint — preventing multiple USDC representations from fragmenting liquidity.

## Deposit / Withdraw Flow

### Depositing SPL → EVM

1. User transfers SPL tokens to the bridge vault
2. Bridge creates the user's ATA on Rome EVM (if not exists)
3. ERC-20 wrapper becomes active — user sees balance in MetaMask

### Withdrawing EVM → SPL

1. User calls the Withdraw precompile (`0x42...16`) on Rome EVM
2. Precompile executes SPL transfer from user's PDA back to their Solana wallet
3. SPL tokens appear in the user's Solana wallet

## PDA Derivation

Every EVM address maps to a Solana PDA that owns their token accounts:

```solidity
import {RomeEVMAccount} from "@rome-protocol/solidity-sdk/contracts/core/RomeEVMAccount.sol";

// Get the Solana PDA for an EVM address
bytes32 userPda = RomeEVMAccount.pda(msg.sender);

// Get the user's ATA for a specific mint
bytes32 ata = AssociatedSplToken.create_associated_token_account(userPda, mintPubkey);
```

## Key Patterns

### Reading SPL Balances from Solidity

```solidity
// Read the raw SPL token account state
ISplToken.Account memory account = SplToken.account_state(tokenAccountPubkey);
uint64 balance = account.amount;
uint8 decimals = SplToken.decimals_eq(mintPubkey, 9); // verify decimals
```

### Transferring Tokens via SPL Precompile

```solidity
// Direct SPL transfer (lower level than ERC-20 wrapper)
SplToken.transfer(recipientAta, mintPubkey, amount);
```

## Gas Token

Each Rome EVM chain has its own gas token — any SPL token chosen at chain registration:

* **RSOL** — default gas token (wrapped SOL)
* Custom tokens — any SPL token, priced via Meteora DAMM V1 pool

Gas tokens are ERC-20 representations of SPL tokens. Transfer Hooks do NOT fire on EVM-internal gas payments.

## Constraints

* SPL Token amounts are `uint64` — max value 18,446,744,073,709,551,615
* Default decimals for new SPL mints: 9
* ERC-20 wrapper symbols must be globally unique per factory
* Allowances use EVM storage (not Solana delegates)

## What's Next

* [Transfer Hooks](/core-concepts/transfer-hooks.md) — EVM logic in Token-2022 transfer hooks
* [Token Wrapping Guide](https://github.com/rome-protocol/docs/blob/main/developer-guides/token-wrapping.md) — deploy your own ERC-20 wrapper
* [Contract Addresses](/reference/contract-addresses.md) — deployed factory addresses


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.rome.builders/core-concepts/token-interop.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
