> 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/vi/cac-khai-niem-cot-loi/token-interop.md).

# Tương tác token

Cách token ERC-20 và SPL thực chất là cùng một account nền tảng trên Rome.

Rome biểu diễn một mã thông báo ERC-20 và mã thông báo SPL gốc của nó như một tài khoản dùng chung. Trang này giải thích cách các token hoạt động trên EVM và Solana.

## Mô hình trạng thái dùng chung

Rome không khóa token ở một bên và đúc các bản sao bọc ở bên kia. Một token ERC-20 trên Rome là một **lớp bọc minh bạch** trên một tài khoản token SPL trên Solana — số dư ERC-20 *là* chính là số dư SPL.

<figure><img src="https://4233397286-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F671PKS8ZQSNqtoC8PGvz%2Fuploads%2Fgit-blob-65af9700f322d308e2cb17da3d107f87b51201a7%2Ftoken-interop.svg?alt=media" alt="An ERC-20 wrapper over the same SPL token account on Solana"><figcaption></figcaption></figure>

* Không có độ trễ bridge — số dư ERC-20 chính là số dư SPL
* Không phân mảnh thanh khoản — DeFi ở cả hai bên thấy cùng một token
* Không có rủi ro bridge — không có ký quỹ riêng để bị khai thác

> Các đường dẫn import bên dưới dùng `@rome-protocol/rome-solidity`. Gói này không có trên npm; hãy cài nó từ [`rome-solidity`](https://github.com/rome-protocol/rome-solidity) repo công khai với `npm install github:rome-protocol/rome-solidity`, hoặc `forge install rome-protocol/rome-solidity` với remapping; các hợp đồng wrapper và factory cũng cần OpenZeppelin dưới Foundry (xem [Triển khai Hợp đồng Solidity](/vi/huong-dan-cho-nha-phat-trien/deploy-solidity.md#using-the-rome-solidity-sdk)). Các interface precompile nằm trong [`contracts/interface.sol`](https://github.com/rome-protocol/rome-solidity/blob/master/contracts/interface.sol).

## Hợp đồng wrapper

`SPL_ERC20` (và biến thể track được cache của nó `SPL_ERC20_cached`, mà factory triển khai cho mọi mint ngoại trừ một mint có transfer hook đã được kích hoạt; xem [các mint Token-2022](#token-2022-mints)) cung cấp đầy đủ giao diện ERC-20 trên một mint SPL:

* `balanceOf()` — đọc số dư ATA của người dùng từ Solana (đối với một địa chỉ hợp đồng, nó đọc sổ cái riêng của wrapper, xem bên dưới)
* `transfer()` / `transferFrom()` — chuyển token trên Solana
* `approve()` / `allowance()` — hạn mức ERC-20 theo từng spender được lưu trong storage EVM (một tài khoản token SPL không có hạn mức kiểu EVM). Chúng tách biệt với cấp quyền một lần bên dưới: để di chuyển token của người dùng bất kỳ lúc nào, wrapper hoạt động như delegate SPL của người đó, và người dùng cấp quyền đó một lần cho mỗi ví
* `totalSupply()` — đọc tổng cung của mint SPL

Một hợp đồng không thể gửi quyền cấp một lần, vì vậy token của một hợp đồng được giữ trong tài khoản token riêng của wrapper và được theo dõi trong sổ cái của wrapper. `balanceOf(contract)` đọc sổ cái đó.

## Factory

`ERC20SPLFactory` triển khai một wrapper cho bất kỳ mint SPL nào:

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

// Triển khai một wrapper, lấy name/symbol từ metadata Token-2022 của mint hoặc Metaplex
address wrapper = factory.add_spl_token_with_metadata(splMint);

// Hoặc chỉ định name/symbol thủ công
address wrapper = factory.add_spl_token_no_metadata(splMint, "USD Coin", "USDC");
```

Địa chỉ factory đang hoạt động, từ `live` các mục trong registry (v0.2.0): Hadrian `0x37c13e4ff21fb914eb24014dcbd0f8dbeaa31078`. Các factory cũ hơn vẫn còn trên chuỗi nhưng được đánh dấu là `không còn dùng` hoặc `đã ngừng hoạt động` trong [registry](https://github.com/rome-protocol/rome-registry/tree/main/chains); đừng dùng chúng. Registry là nguồn sự thật, vì vậy hãy đọc địa chỉ đang hoạt động từ đó (xem [Đọc danh sách hợp đồng](/vi/tai-lieu-tham-khao/contract-addresses.md#reading-the-contracts-list)) thay vì sao chép một địa chỉ vào code của bạn.

## Các mint chuẩn

Không có hợp đồng token-registry trên chuỗi. Các wrapper chuẩn và token gas/bridge được tuyển chọn trong [`rome-protocol/registry`](https://github.com/rome-protocol/rome-registry)ngoài chuỗi; các wrapper không cần cấp quyền được tạo qua `add_spl_token_no_metadata` được phát hiện từ sự kiện on-chain `TokenCreated` . Điều này giữ mỗi tài sản được ánh xạ tới một mint SPL chuẩn duy nhất mà không làm phân mảnh thanh khoản.

## Các thao tác SPL từ Solidity

Đối với các primitive SPL được người dùng-PDA ký, hãy dùng **HelperProgram** precompile (`0xFF…09`) — tạo ATA, chuyển SPL và quy đổi gas↔lamports:

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

IHelperProgram helper = IHelperProgram(0xFF00000000000000000000000000000000000009);

helper.create_ata(user, mint);              // tạo ATA của người dùng cho một mint
helper.transfer_spl(to, tokens, mint);      // chuyển SPL từ PDA của bên gọi
```

`transfer_spl` có nhiều overload (bao gồm một biến thể delegate cho `transferFrom` các luồng); xem `interface.sol` để biết chữ ký chính xác. Trên track được cache, các thao tác tương đương nằm trên `ISplCached` (`0xFF…05`) và `IAssociatedSplCached` (`0xFF…06`). Một hợp đồng dùng một track một cách nhất quán.

## Cấp quyền một lần cho mỗi ví

Wrapper di chuyển token của người dùng với tư cách là **SPL delegate** của tài khoản token của người dùng đó. Vì vậy, một ví phải cấp cho nó quyền đó một lần cho mỗi wrapper, trước khi wrapper có thể di chuyển bất cứ thứ gì cho nó. Việc cấp quyền là một lần gọi tới Helper precompile (`0xff00000000000000000000000000000000000009`):

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

`approve_spl` được ký bởi chính tài khoản của người dùng, vì vậy chính người dùng gửi nó, không phải wrapper hay hợp đồng của bạn. Mint là `mint_id()` trên wrapper.

* **Ai cần nó.** Mọi ví EVM, và mọi địa chỉ tổng hợp của người dùng Solana, cho mỗi wrapper mà nó muốn di chuyển token. Một hợp đồng kéo token bằng `transferFrom` chỉ hoạt động với những người dùng đã cấp quyền cho wrapper.
* **Cách kiểm tra.** Gọi `isEnabled(user)` trên wrapper. Nó trả về `trong khi còn bất kỳ phần nào, vì vậy ứng dụng sẽ không biết để hỏi lại.` khi quyền cấp đã được thiết lập.
* **Nó trông như thế nào khi bị thiếu.** Việc chuyển bị hoàn tác với lỗi SPL `0x4` (không khớp chủ sở hữu).
* **Một delegate cho mỗi tài khoản token.** Một tài khoản token SPL chỉ có một delegate. Cấp quyền cho một chương trình khác trên cùng tài khoản token đó, như bridge hoặc router, sẽ thay thế quyền của wrapper, và wrapper ngừng hoạt động cho người dùng đó cho đến khi họ cấp lại quyền. Hãy kiểm tra `vẫn` trước khi chuyển thay vì cho rằng quyền cấp trước đó vẫn còn.

Các [hướng dẫn ứng dụng hai làn](/vi/huong-dan-cho-nha-phat-trien/dual-lane-app.md#one-time-grant-the-wrapper) có mã đầy đủ cho cả hai làn.

## Nạp và rút

* **Vào EVM** — phía SPL ghi có vào ATA thuộc PDA của người dùng; wrapper ERC-20 phản ánh số dư ngay lập tức. Chuyển vào từ chuỗi khác được quyết toán không cần tin cậy thông qua một ủy quyền do người dùng ký trên bridge.
* **Bọc gas thành token gas** — gọi `Rút` precompile (`0x42…16`). `withdraw_to_ata(wei)` đốt gas native và ghi có số lượng tương đương, theo số thập phân riêng của token gas, vào tài khoản token của bên gọi, sau đó wrapper của token gas (ví dụ wUSDC) hiển thị nó như một số dư. `withdraw_to_pda(wei)` trả lamports cho PDA của bên gọi và chỉ áp dụng cho chuỗi có gas là SOL; nó hoàn tác trên Hadrian và Rubicon, nơi dùng USDC làm gas, vì vậy hãy dùng `withdraw_to_ata` ở đó. Số lượng tính theo đơn vị gas 18 chữ số thập phân và phải là một số nguyên của đơn vị token gas: trên chuỗi gas USDC thì phải là bội số của 10^12 (một đơn vị 6 chữ số thập phân), nếu không lời gọi sẽ hoàn tác. `HelperProgram.deposit_from_ata(wei)` đi theo chiều ngược lại và biến token gas trong tài khoản token của bên gọi trở lại gas native, với cùng quy tắc bội số. Đây là các phép quy đổi token gas, không phải lối thoát cho các token khác.
* **Ra Solana, bất kỳ token nào khác** — số dư riêng của wrapper vốn đã là số dư SPL, nên chuyển nó ra là một lần chuyển SPL:
  * `HelperProgram.transfer_spl_to_signer(amount, mint)` gửi token của bên gọi tới tài khoản token của ví Solana đã ký giao dịch. Đây là bước hồi chuyển cho người dùng ví Solana.
  * `RomeBridgeWithdraw.bridgeOutToSolana(solanaRecipient, amount, mint)` gửi chúng tới bất kỳ ví Solana nào. Nó cần quyền cấp của người dùng cho hợp đồng bridge (`approve_spl(bridge, …)`xem ghi chú một delegate ở trên), và tài khoản token của người nhận phải tồn tại; `ensureRecipientAta(solanaRecipient, mint)` tạo nó.
  * `RomeBridgeWithdraw` cũng đốt USDC sang chuỗi khác qua CCTP (`burnUSDC`) và gửi tài sản khác qua Wormhole (`burnETH`, `burnToWormhole`).

Hãy lấy `RomeBridgeWithdraw` địa chỉ đang hoạt động từ registry.

## Token gas

Mỗi chuỗi có token gas riêng — bất kỳ token SPL nào, được định giá qua một pool Meteora DAMM (v1 hoặc v2, có thể cấu hình). Các chuỗi công khai (Rubicon và Hadrian) dùng USDC; trên Hadrian là USDC thử nghiệm, trên Rubicon là USDC thật. Không có token gas mặc định chung.

## các mint Token-2022

Factory cũng bọc các mint Token-2022. Nó hỏi Helper precompile mint đó là gì, bằng `mint_info(mint)` (selector `0xe24bf5d4`), và chọn wrapper từ câu trả lời. `mint_info` trả về năm giá trị:

| Giá trị        | Ý nghĩa                                                                                              |
| -------------- | ---------------------------------------------------------------------------------------------------- |
| `tokenProgram` | chương trình token sở hữu mint (chương trình SPL Token cổ điển hoặc Token-2022)                      |
| `decimals`     | số thập phân của mint                                                                                |
| `hookProgram`  | chương trình transfer-hook, hoặc bằng không khi không có hook hoặc hook có nhưng chưa được kích hoạt |
| `feeBps`       | phí chuyển đổi tính theo basis point, hoặc bằng không khi không có phí hoặc đã đặt về 0              |
| `extensions`   | một bitmap của các extension Token-2022 mà mint có, bit N cho loại extension N                       |

Mint nhận được gì:

| Mint                                                                                                                | Wrapper                                                                                              | `wrapper_kind_by_mint` |
| ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------- |
| Một mint thông thường, một mint Token-2022 không có hook, hoặc một mint có hook hiện diện nhưng chưa được kích hoạt | wrapper được cache (`SPL_ERC20_cached`)                                                              | `1`                    |
| Một mint Token-2022 với transfer hook đã được kích hoạt                                                             | wrapper có hook (`SPL_ERC20_Token2022Hooked`), được triển khai thông qua `hooked_wrapper_deployer()` | `2`                    |

Một mint có phí chuyển đổi sẽ nhận wrapper được cache, và `Transfer` sự kiện của nó báo cáo số lượng đến sau khi trừ phí, không phải số lượng đã gửi. Bạn có thể đọc loại wrapper mà một mint nhận được từ factory bằng `của factory (địa chỉ factory nằm trong`; trên factory Hadrian đang hoạt động, các wrapper USDC, ETH và SOL đều trả về `1`.

Những gì bị từ chối:

* Một mint có transfer hook đã được kích hoạt không thể nhận một trong các wrapper thông thường. Triển khai `SPL_ERC20` hoặc `SPL_ERC20_cached` trực tiếp trên nó sẽ hoàn tác với `ArmedTransferHookUnsupported(mint, hookProgram)`. Thông qua factory, trường hợp này sẽ đi tới wrapper có hook thay thế.
* Một địa chỉ không phải là mint SPL làm cho `mint_info` hoàn tác với `Invalid SPL mint account: <address>`.
* Một mint đã có wrapper sẽ hoàn tác với `Token exists`, và một symbol đã được dùng sẽ hoàn tác với `Token with symbol exists`.
* `add_spl_token_with_metadata` đọc tên và symbol trước tiên từ metadata Token-2022 của chính mint, và từ Metaplex sau đó. Nếu mint không có cả hai thì nó sẽ hoàn tác với `Mint asserts no name: use add_spl_token_no_metadata`.

Cách wrapper có hook hoạt động:

* Token-2022 chạy hook trên mọi lần chuyển, và hook cần các tài khoản phụ thuộc vào lần chuyển đó. Wrapper không thể tự suy ra chúng, nên bên gọi sẽ giải quyết danh sách tài khoản đầy đủ từ danh sách extra-account-metas của mint và truyền vào. Wrapper kiểm tra phần cố định của danh sách đó (chương trình hook và tài khoản xác thực của nó) và chuyển tiếp nguyên trạng.
* Bản `chuyển`, `transferFrom` và `bridgeOutToSolana` hoàn tác với `HookAccountPlanRequired`. Hãy dùng `transferWithHookAccounts`, `transferFromWithHookAccounts` và `bridgeOutToSolanaWithHookAccounts`, những hàm này nhận danh sách tài khoản như một đối số bổ sung.
* Nếu chương trình hook của mint thay đổi sau khi wrapper được triển khai, các lần chuyển sẽ hoàn tác với `HookConfigurationChanged`.
* Nó di chuyển token bằng lời gọi trực tiếp xuyên chương trình, theo track cũ, và không dùng track được cache. Một hợp đồng dùng một track nhất quán, và các lời gọi trên track cũ chỉ chạy trong thực thi nguyên tử, vì vậy một lời gọi dùng wrapper có hook phải nằm gọn trong một giao dịch nguyên tử.
* Nó không giữ sổ cái cho số dư của hợp đồng. Token gửi tới một hợp đồng sẽ vào tài khoản token riêng của hợp đồng, và hợp đồng chỉ có thể gửi chúng đi sau khi đã thực hiện `approve_spl` quyền cấp cho chính wrapper.
* Nó cần cùng [quyền cấp một lần](#one-time-grant-per-wallet) như bất kỳ wrapper nào khác.

## Ràng buộc

* Số lượng token SPL là `uint64` (tối đa 18,446,744,073,709,551,615)
* Hạn mức ERC-20 (`approve` / `allowance`) là storage EVM. Wrapper di chuyển token với tư cách là SPL delegate của người dùng, thứ mà người dùng cấp một lần cho mỗi wrapper (xem [Cấp quyền một lần cho mỗi ví](#one-time-grant-per-wallet))
* symbol của wrapper ERC-20 phải là duy nhất cho mỗi factory

## Tiếp theo là gì

* [Địa chỉ Hợp đồng](/vi/tai-lieu-tham-khao/contract-addresses.md) — precompile và địa chỉ theo từng chain
* [Gọi Solana từ EVM](/vi/huong-dan-cho-nha-phat-trien/call-solana-from-evm.md) — CPI và các thao tác SPL từ Solidity


---

# 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/vi/cac-khai-niem-cot-loi/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.
