> For the complete documentation index, see [llms.txt](https://docs.rome.builders/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rome.builders/id/konsep-inti/token-interop.md).

# Interoperabilitas Token

Bagaimana token ERC-20 dan SPL adalah akun dasar yang sama di Rome.

Rome merepresentasikan token ERC-20 dan token SPL dasarnya sebagai satu akun bersama. Halaman ini menjelaskan cara kerja token di seluruh EVM dan Solana.

## Model keadaan bersama

Untuk dompet biasa, token ERC-20 di Rome adalah sebuah **pembungkus transparan** di atas akun token SPL di Solana. Saldo ERC-20 dompet adalah saldo SPL-nya. Sebuah alamat turunan program untuk alamat EVM memiliki akun token tersebut. Ini adalah **PDA otoritas eksternal**.

<figure><img src="https://320084446-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>

* Tanpa penundaan bridging: saldo ERC-20 dompet adalah saldo SPL-nya.
* Dompet biasa, termasuk dompet MetaMask dan alamat sintetis, tidak memiliki fragmentasi likuiditas. Setiap pembungkus atas suatu mint menampilkan saldo dompet yang sama. Fungsi `balanceOf` membaca saldo itu dari akun token dompet. Token yang disimpan di dalam kontrak EVM disimpan per pembungkus (lihat [Kontrak pembungkus](#the-wrapper-contract)).
* Untuk token milik dompet, tidak ada yang terkunci di satu sisi dan dicetak di sisi lain. Token yang dipegang kontrak berada di akun token escrow milik wrapper itu sendiri di Solana. Setiap wrapper melacak token tersebut secara terpisah.

**Mengapa wrapper kedua tidak memisahkan saldo**

Dua wrapper atas mint yang sama menampilkan saldo dompet yang sama. Wrapper membaca `totalSupply` dari mint Solana. Pada Hadrian (testnet), tiga wrapper wUSDC mengembalikan saldo dompet yang cocok. Ketiga wrapper wUSDC (9.0.0, 8.0.0, dan Mei 2026) mengembalikan suplai mint Solana.

Setiap factory mengizinkan satu wrapper per mint. Versi factory baru dapat menerapkan wrapper lain untuk mint itu. Registry menamai wrapper kanonik per chain.

Dompet Solana yang memegang USDC secara langsung dan sebuah alamat EVM memegang token dari mint yang sama. Pool Solana menggunakan vault dan cadangan yang sama untuk keduanya.

Tabel pemegang di dalam [Melalui explorer](/id/aplikasi-di-rome/via.md) dibangun dari `Transfer` peristiwa. Itu dapat berbeda dari saldo akun token karena transfer SPL langsung, `ActivateAta`, dan transfer melalui wrapper lain tidak muncul dalam peristiwa wrapper tersebut.

> Jalur impor di bawah menggunakan `@rome-protocol/rome-solidity`. Paket ini tidak ada di npm; instal dari [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo publik dengan `npm install github:rome-protocol/rome-solidity`, atau `forge install rome-protocol/rome-solidity` dengan remapping; kontrak wrapper dan factory juga memerlukan OpenZeppelin di bawah Foundry (lihat [Deploy Solidity Contracts](/id/panduan-pengembang/deploy-solidity.md#using-the-rome-solidity-sdk)). Interface precompile berada di [`contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol).

## Kontrak pembungkus

`SPL_ERC20` (dan varian track cache-nya `SPL_ERC20_cached`, yang diterapkan factory untuk setiap mint kecuali satu dengan transfer hook yang diaktifkan; lihat [Mint Token-2022](#token-2022-mints)) menyediakan interface ERC-20 lengkap di atas mint SPL:

* `balanceOf()` — membaca saldo ATA pengguna dari Solana (untuk alamat kontrak, ia membaca ledger milik wrapper itu sendiri, lihat di bawah)
* `transfer()` / `transferFrom()` — memindahkan token di Solana
* `approve()` / `allowance()` — allowance ERC-20 per-pengelola yang disimpan di storage EVM (akun token SPL tidak memiliki allowance bergaya EVM). Ini terpisah dari pemberian satu kali di bawah: untuk memindahkan token pengguna sama sekali, wrapper bertindak sebagai delegate SPL pengguna itu, dan pengguna memberinya hak itu sekali per dompet
* `totalSupply()` — membaca suplai mint SPL

Sebuah kontrak tidak dapat mengirim pemberian satu kali itu, jadi token sebuah kontrak disimpan di akun token milik wrapper sendiri dan dilacak di ledger wrapper. `balanceOf(contract)` membaca ledger itu.

## Factory

`ERC20SPLFactory` menerapkan sebuah wrapper untuk setiap mint SPL:

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

// Terapkan wrapper, memuat nama/simbol dari metadata Token-2022 mint atau Metaplex
address wrapper = factory.add_spl_token_with_metadata(splMint);

// Atau tentukan nama/simbol secara manual
address wrapper = factory.add_spl_token_no_metadata(splMint, "USD Coin", "USDC");
```

Alamat factory aktif, dari `live` entri registry (v0.2.0): Hadrian `0x37c13e4ff21fb914eb24014dcbd0f8dbeaa31078`. Factory sebelumnya masih ada on-chain tetapi ditandai `deprecated` atau `pensiun` di [registry](https://github.com/rome-protocol/rome-registry/tree/main/chains); jangan gunakan mereka. Registry adalah sumber kebenaran, jadi baca alamat aktif dari sana (lihat [Membaca daftar kontrak](/id/referensi/contract-addresses.md#reading-the-contracts-list)) alih-alih menyalin satu ke kode Anda.

## Mint kanonik

Tidak ada kontrak registry token on-chain. Wrapper kanonik dan token gas/jembatan dikurasi dalam [`rome-protocol/registry`](https://github.com/rome-protocol/rome-registry); wrapper permissionless yang dibuat melalui `add_spl_token_no_metadata` ditemukan dari event on-chain `TokenCreated` event. Registry menamai wrapper kanonik untuk setiap mint pada setiap chain. Saldo dompet dibagi di seluruh wrapper. Setiap wrapper secara terpisah melacak saldo yang dipegang oleh kontrak EVM.

## Operasi SPL dari Solidity

Untuk primitive SPL yang ditandatangani user-PDA, gunakan **HelperProgram** precompile (`0xFF…09`) — pembuatan ATA, transfer SPL, dan konversi gas↔lamports:

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

IHelperProgram helper = IHelperProgram(0xFF00000000000000000000000000000000000009);

helper.create_ata(user, mint);              // membuat ATA pengguna untuk sebuah mint
helper.transfer_spl(to, tokens, mint);      // mentransfer SPL dari PDA pemanggil
```

`transfer_spl` memiliki beberapa overload (termasuk varian delegate untuk `transferFrom` alur); lihat `interface.sol` untuk signature yang tepat. Pada track cache, operasi yang setara berada di `ISplCached` (`0xFF…05`dan `IAssociatedSplCached` (`0xFF…06`). Sebuah kontrak menggunakan satu track secara konsisten.

## Pemberian satu kali per dompet

Wrapper memindahkan token pengguna sebagai **delegate SPL** dari akun token pengguna itu. Karena itu wallet harus memberinya hak itu sekali per wrapper, sebelum wrapper dapat memindahkan apa pun untuknya. Pemberian itu adalah satu panggilan ke Helper precompile (`0xff00000000000000000000000000000000000009`):

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

`approve_spl` ditandatangani oleh akun pengguna sendiri, jadi pengguna yang mengirimnya, bukan wrapper atau kontrak Anda. Mint adalah `mint_id()` di wrapper.

* **Siapa yang membutuhkannya.** Setiap wallet EVM, dan setiap alamat sintetis pengguna Solana, untuk setiap wrapper yang tokennya ingin dipindahkan. Sebuah kontrak yang menarik token dengan `transferFrom` hanya berfungsi untuk pengguna yang telah memberikan grant kepada wrapper.
* **Cara memeriksa.** Panggil `isEnabled(user)` pada wrapper. Ini mengembalikan `selama masih ada sebagian darinya, jadi aplikasi tidak akan tahu untuk meminta lagi.` setelah grant tersedia.
* **Seperti apa jika hilang.** Transfer dibatalkan dengan error SPL `0x4` (ketidaksesuaian owner).
* **Satu delegate per akun token.** Sebuah akun token SPL memiliki satu delegate. Memberikan grant pada program lain ke akun token yang sama, seperti bridge atau router, menggantikan grant wrapper, dan wrapper berhenti berfungsi untuk pengguna itu sampai mereka memberinya lagi. Periksa `tetap` sebelum transfer, alih-alih mengasumsikan grant sebelumnya masih ada.

Ketika versi factory baru menggantikan sebuah wrapper, setiap wallet memberikan izin kepada wrapper baru dengan `approve_spl`. Wrapper lama hanya dapat memindahkan token wallet itu selama ia tetap menjadi delegate akun token. Memberikan izin pada wrapper baru menggantikan delegate itu.

Panduan [panduan aplikasi dua jalur](/id/panduan-pengembang/dual-lane-app.md#one-time-grant-the-wrapper) berisi kode lengkap untuk kedua jalur.

## Deposit dan tarik

* **Ke EVM** — sisi SPL mengkredit ATA milik PDA pengguna; wrapper ERC-20 langsung merefleksikan saldo tersebut. Transfer masuk lintas chain diselesaikan tanpa kepercayaan melalui otorisasi yang ditandatangani pengguna pada bridge.
* **Bungkus gas menjadi token gas** — panggil `Withdraw` precompile (`0x42…16`). `withdraw_to_ata(wei)` membakar gas native dan mengkredit jumlah yang setara, dalam desimal token gas itu sendiri, ke akun token pemanggil, yang kemudian ditampilkan sebagai saldo oleh wrapper token gas (misalnya wUSDC). `withdraw_to_pda(wei)` membayar lamport ke PDA pemanggil dan hanya berlaku untuk chain yang gas-nya SOL; ini dibatalkan di Hadrian dan Rubicon, yang menggunakan USDC sebagai gas, jadi gunakan `withdraw_to_ata` di sana. Jumlahnya dalam unit gas 18 desimal dan harus merupakan bilangan bulat unit token gas: pada chain gas USDC, kelipatan 10^12 (satu unit 6 desimal), kalau tidak panggilan akan dibatalkan. `HelperProgram.deposit_from_ata(wei)` bergerak ke arah sebaliknya dan mengubah token gas di akun token pemanggil kembali menjadi gas native, dengan aturan kelipatan yang sama. Ini adalah konversi token gas, bukan keluaran untuk token lain.
* **Keluar ke Solana, token lain apa pun** — saldo milik wrapper sendiri sudah merupakan saldo SPL, jadi memindahkannya keluar adalah transfer SPL:
  * `HelperProgram.transfer_spl_to_signer(amount, mint)` mengirim token pemanggil ke akun token dari wallet Solana yang menandatangani transaksi. Jalur balik ini hanya mencapai wallet milik pengguna pada jalur Solana yang ditandatangani wallet. Jika dipanggil dari MetaMask atau melalui relay, penandatangan adalah proxy payer milik Rome, sehingga panggilan gagal atau token masuk ke Rome. Gunakan `transfer_spl` atau `RomeBridgeWithdraw.bridgeOutToSolana` di sana sebagai gantinya.
  * `RomeBridgeWithdraw.bridgeOutToSolana(solanaRecipient, amount, mint)` mengirimnya ke wallet Solana mana pun. Ini memerlukan grant pengguna untuk kontrak bridge (`approve_spl(bridge, …)`, lihat catatan satu delegate di atas), dan akun token penerima harus ada; `ensureRecipientAta(solanaRecipient, mint)` membuatnya.
  * `RomeBridgeWithdraw` juga membakar USDC ke chain lain melalui CCTP (`burnUSDC`) dan mengirim aset lain melalui Wormhole (`burnETH`, `burnToWormhole`).

Ambil yang aktif `RomeBridgeWithdraw` alamat dari registry.

## Token gas

Setiap chain memiliki token gas-nya sendiri: SOL atau token SPL. Sumber harga Rome Proxy dapat berupa feed API Hermes, pool Meteora DAMM v1, pool Meteora DAMM v2, atau harga tetap. Hadrian (testnet) dan Rubicon (mainnet) menggunakan USDC. Hadrian menggunakan USDC uji. Rubicon menggunakan USDC asli. Tidak ada token gas default universal.

## Mint Token-2022

Factory juga membungkus mint Token-2022. Ia meminta Helper precompile apa mint itu, dengan `mint_info(mint)` (selector `0xe24bf5d4`), dan memilih wrapper dari jawabannya. `mint_info` mengembalikan lima nilai:

| Nilai          | Arti                                                                                        |
| -------------- | ------------------------------------------------------------------------------------------- |
| `tokenProgram` | program token yang memiliki mint (program SPL Token klasik atau Token-2022)                 |
| `decimals`     | jumlah desimal mint                                                                         |
| `hookProgram`  | program transfer-hook, atau nol ketika tidak ada hook atau hook ada tetapi belum diaktifkan |
| `feeBps`       | biaya transfer dalam basis poin, atau nol ketika tidak ada atau disetel ke nol              |
| `extensions`   | bitmap ekstensi Token-2022 yang dimiliki mint, bit N untuk tipe ekstensi N                  |

Apa yang didapat sebuah mint:

| Mint                                                                                          | Wrapper                                                                                      | `wrapper_kind_by_mint` |
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------- |
| Sebuah mint biasa, mint Token-2022 tanpa hook, atau yang hook-nya ada tetapi belum diaktifkan | wrapper cached (`SPL_ERC20_cached`)                                                          | `1`                    |
| Sebuah mint Token-2022 dengan transfer hook yang diaktifkan                                   | wrapper hooked (`SPL_ERC20_Token2022Hooked`), diterapkan melalui `hooked_wrapper_deployer()` | `2`                    |

Sebuah mint dengan biaya transfer mendapat wrapper cached, dan event `Transfer` melaporkan jumlah yang tiba setelah biaya, bukan jumlah yang dikirim. Anda dapat membaca jenis wrapper yang didapat mint dari factory dengan `milik factory (alamat factory ada di`; pada factory Hadrian yang aktif, wrapper USDC, ETH, dan SOL semuanya mengembalikan `1`.

Apa yang ditolak:

* Mint dengan transfer hook yang diaktifkan tidak dapat memperoleh salah satu wrapper biasa. Menerapkan `SPL_ERC20` atau `SPL_ERC20_cached` pada mint itu secara langsung dibatalkan dengan `ArmedTransferHookUnsupported(mint, hookProgram)`. Melalui factory, kasus ini dialihkan ke wrapper hooked sebagai gantinya.
* Sebuah alamat yang bukan mint SPL membuat `mint_info` dibatalkan dengan `Invalid SPL mint account: <address>`.
* Mint yang sudah memiliki wrapper dibatalkan dengan `Token exists`, dan simbol yang sudah dipakai dibatalkan dengan `Token with symbol exists`.
* `add_spl_token_with_metadata` membaca nama dan simbol pertama-tama dari metadata Token-2022 milik mint itu sendiri dan kedua dari Metaplex. Jika mint tidak memiliki keduanya, ini dibatalkan dengan `Mint asserts no name: use add_spl_token_no_metadata`.

Cara kerja wrapper hooked:

* Token-2022 menjalankan hook pada setiap transfer, dan hook memerlukan akun yang bergantung pada transfer. Wrapper tidak dapat menentukannya, jadi pemanggil menyelesaikan daftar akun lengkap dari daftar extra-account-metas mint dan menyerahkannya. Wrapper memeriksa bagian tetap dari daftar itu (program hook dan akun validasinya) dan meneruskannya tanpa perubahan.
* Yang biasa `transfer`, `transferFrom` dan `bridgeOutToSolana` dibatalkan dengan `HookAccountPlanRequired`. Gunakan `transferWithHookAccounts`, `transferFromWithHookAccounts` dan `bridgeOutToSolanaWithHookAccounts`, yang mengambil daftar akun sebagai argumen tambahan.
* Jika program hook mint berubah setelah wrapper diterapkan, transfer dibatalkan dengan `HookConfigurationChanged`.
* Ia memindahkan token lewat panggilan lintas-program langsung, track legacy, dan tidak menggunakan track cache. Sebuah kontrak menggunakan satu track secara konsisten, dan panggilan pada track legacy hanya berjalan dalam eksekusi atomik, jadi panggilan yang menggunakan wrapper hooked harus muat dalam satu transaksi atomik.
* Ia tidak menyimpan ledger untuk saldo kontrak. Token yang dikirim ke kontrak masuk ke akun token milik kontrak itu sendiri, dan kontrak hanya dapat mengirimkannya setelah membuat `approve_spl` grant ke wrapper itu sendiri.
* Ia memerlukan [grant satu kali](#one-time-grant-per-wallet) yang sama seperti wrapper lainnya.

## Batasan

* Jumlah token SPL adalah `uint64` (maksimal 18.446.744.073.709.551.615)
* Allowance ERC-20 (`approve` / `allowance`) adalah storage EVM. Wrapper memindahkan token sebagai delegate SPL pengguna, yang diberikan pengguna sekali per wrapper (lihat [Pemberian satu kali per dompet](#one-time-grant-per-wallet))
* Symbol wrapper ERC-20 harus unik per factory

## Apa Selanjutnya

* [Contract Addresses](/id/referensi/contract-addresses.md) — precompile dan alamat per-chain
* [Call Solana from EVM](/id/panduan-pengembang/call-solana-from-evm.md) — CPI dan operasi SPL dari Solidity
* [Cara Solana dan EVM bertemu di Rome](/id/konsep-inti/how-solana-and-evm-meet.md)
* [Integrasikan program Solana](/id/panduan-pengembang/integrate-solana-program.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

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

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.rome.builders/id/konsep-inti/token-interop.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

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