> 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/ung-dung-tren-rome/bloom/building.md).

# Xây dựng trên Bloom

Hướng dẫn này trả lời một câu hỏi: **builder tự làm việc này như thế nào?** Lấy một hợp đồng tài sản thực tế viết bằng Solidity, triển khai nó trên chuỗi Rome dưới dạng token có cấp phép, mở nó cho cả ví Solana lẫn ví EVM mà không cần cầu nối và không cần token thứ hai, và tích hợp quyết định KYC/tuân thủ của riêng bạn.

Đối tượng là một nhà phát triển đang đánh giá hoặc áp dụng cách tiếp cận này. Bloom là ví dụ xuyên suốt — tài sản đang hoạt động của nó **ARCV ("Mineral Vault I")** trên Hadrian (`200010`) là một bản thể hiện thực của mọi bước bên dưới, và biên nhận triển khai của nó (`deployments/200010.tokens/ARCV.json`) ghi lại chính xác ba hợp đồng và tám giao dịch mà phương pháp này tạo ra.

Không có gì ở đây sửa đổi các hợp đồng tài sản. Bloom chạy framework Arc của Plume **nguyên bản** (cây mã được vendored trong `contracts/`, giống byte-for-byte với `plumenetwork/contracts` ở bản ghim trong `NOTICE`); hợp đồng duy nhất phía Rome là `ArcTokenFactoryV2`, bổ sung một hàm. Công việc nằm ở *làm thế nào* bạn triển khai và *làm thế nào* bạn điều khiển các hợp đồng — chứ không phải sửa đổi chúng.

> **Phương pháp này, nhìn thoáng qua.** Một token có cấp phép là **ba** hợp đồng đã được triển khai. Nó được triển khai **theo từng phần** — một hợp đồng cho mỗi giao dịch — vì một lệnh gọi factory trong một giao dịch vượt quá giới hạn account trên mỗi giao dịch của Rome. Các proxy của nó phải đến từ **các artifact bị khóa nguồn gốc** nếu không việc đăng ký on-chain sẽ thất bại an toàn. Tuân thủ là một **boolean cho mỗi địa chỉ**, do bất kỳ ai mà quy trình KYC của bạn cho phép ghi. Và cùng một lần triển khai đó có thể truy cập từ ví Solana thông qua một **người gửi tổng hợp** được suy ra trên-chain — không cầu nối, không token bọc, không danh sách cho phép thứ hai.

## Điều kiện tiên quyết

> **Trước khi bạn bắt đầu.** Bộ công cụ cho issuer mà hướng dẫn này dùng hiện chưa được công bố. Điều đó bao gồm các script (`deploy-infra.ts`, `create-token.ts` và phần còn lại), các `contracts/` và `rome-contracts/` cây mã, và các biên nhận triển khai mà nó đặt tên. Các issuer nhận bộ công cụ này bằng cách onboarding với Rome. Cho đến lúc đó, hãy đọc hướng dẫn này như mô tả về phương pháp và về chức năng của từng script.

Bộ công cụ tham chiếu nằm trong một `scripts/` thư mục (được cung cấp khi onboarding; cài phụ thuộc của nó bằng `npm install`). Mỗi lệnh đều lấy thông tin chuỗi từ registry thay vì hardcode, nên chuyển sang một chuỗi Rome khác chỉ là thay đổi môi trường, không phải thay đổi mã:

```bash
export PRIVATE_KEY=…       # khóa deployer/issuer có funding trên chuỗi mục tiêu
export CHAIN_ID=200010     # chọn chuỗi
export REGISTRY_ROOT=…     # một checkout của registry chuỗi Rome
# Các luồng trên kênh Solana cũng cần:
export SOLANA_KEYPAIR=…    # đường dẫn tới một JSON keypair Solana đã có funding (fee payer)
```

Gas trên các chuỗi Rome được tính bằng USDC; toàn bộ chuỗi triển khai tốn cỡ 10–15 đơn vị gốc trên devnet. Khóa chỉ đến từ môi trường — không bao giờ từ repo. Hãy build hợp đồng một lần trước khi triển khai, vì các artifact **phải** đến từ `rome-contracts/out` (xem bước 3):

```bash
cd contracts       && forge build --via-ir     # bộ Arc được vendored
cd rome-contracts  && forge build --via-ir     # ArcTokenFactoryV2 + proxy chuẩn tắc
Bước 1 — Hiểu một token có cấp phép
```

***

## Bước 1 — Hiểu một token có cấp phép *là* ở đây

Một token Bloom có cấp phép không phải là một hợp đồng. Nó là **ba hợp đồng đã được triển khai**, cộng thêm bốn hợp đồng dùng chung mà ops triển khai một lần cho mỗi chuỗi và mọi issuer đều dùng lại.

**Ba hợp đồng theo từng token** — một tài sản sở hữu chính xác những cái này:

| Hợp đồng                     | Nguồn                                                       | Nó là gì                                                                                                                         |
| ---------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | Chính token — một proxy UUPS trỏ tới phần triển khai ArcToken dùng chung. `ArcToken` Số dư, người nắm giữ, và vai trò nằm ở đây. |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | Mô-đun danh sách cho phép. Một boolean cho mỗi địa chỉ; được áp dụng trên mọi lần chuyển khi tài sản bị khóa.                    |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | Mô-đun lợi suất. Loại trừ các địa chỉ khỏi phân phối lợi suất mà không động vào số dư nắm giữ của chúng.                         |

**Bốn hợp đồng dùng chung, một cái cho mỗi chuỗi** (được triển khai bởi `deploy-infra.ts`):

| Hợp đồng                     | Vai trò                                                                    |
| ---------------------------- | -------------------------------------------------------------------------- |
| `RestrictionsRouter`         | Sổ đăng ký loại mô-đun — nối dây chuỗi, thiết lập một lần bởi ops.         |
| `ArcTokenFactoryV2`          | Đăng ký + codehash proxy chuẩn tắc (bước 2–3).                             |
| `ArcToken` (phần triển khai) | Logic tài sản dùng chung, có thể tái sử dụng mà mọi token đều proxy tới.   |
| `ArcTokenPurchase`           | Một storefront duy nhất, được dùng chung bởi mọi token và issuer (bước 4). |
| wUSDC                        | Phần tiền mặt — đồng tiền lợi suất và đồng tiền mua-bán.                   |

Hạn chế chuyển và hạn chế lợi suất là **các mô-đun tách biệt có chủ đích**: một người nắm giữ có thể bị chặn nhận thu nhập trong khi vẫn giữ tài sản, và đó chính là cách lệnh trừng phạt và lệnh của tòa thực sự vận hành.

Sự thật "ba hợp đồng" này là yếu tố sống còn cho việc xác minh. `verify-deployments.ts` và `register-asset.ts` đều kỳ vọng một token quy về ba hợp đồng đã triển khai — từng có lần việc xác minh thủ công một token đã bỏ sót bảy cái trên một chuỗi (mọi mô-đun lợi suất và ba mô-đun whitelist).

Vòng đời có ba giai đoạn và một cánh cửa một chiều:

```
nháp     proxy đang hoạt động, chưa liên kết mô-đun — chưa có gì chặn giao dịch chuyển
  ↓
không bị khóa   mô-đun đã liên kết, transfersAllowed = true   (WhitelistRestrictions.initialize đặt giá trị này)
  ↓  đúc nguồn cung, xây allowlist               [có thể đảo ngược]
đã khóa     setTransfersAllowed(false)                [CỬA MỘT CHIỀU — nguồn cung trở nên cố định]
```

Việc khóa là một chiều đối với nguồn cung vì mint và burn đi qua `address(0)` làm đối tác, và `address(0)` không bao giờ có thể được thêm vào allowlist — nên một khi đã khóa, sẽ không bao giờ đúc được nguồn cung mới. Đó là sự bảo đảm mà một tài sản có cấp phép đưa ra cho người nắm giữ.

***

## Bước 2 — Triển khai theo từng phần, không bao giờ trong một giao dịch

Arc cung cấp một `createToken` thao tác một lần cho phép triển khai proxy và cả hai mô-đun rồi nối dây chúng trong một lệnh gọi duy nhất. **Trên Rome bạn không thể dùng nó.** Giao dịch đó cần **77 khóa tài khoản Solana**, và giới hạn trên mỗi giao dịch của Rome là **62 khóa**. Nó không bao giờ vừa — không phải bằng tinh chỉnh, không phải bằng một bước tối ưu hóa.

Vì vậy việc triển khai là **theo từng phần**: mỗi hợp đồng ở một giao dịch riêng, và token đạt trạng thái tương đương factory thông qua `ArcTokenFactoryV2.registerToken` thay vì quy trình khổng lồ `createToken`. Đây là create wizard, và đó là thứ console của issuer thực hiện từng bước:

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

Bản triển khai tham chiếu là `arc/plan/create.ts` (`createSequence()`), mà ứng dụng hiển thị thành sáu thẻ (`bloom/lib/createCards.ts`) qua **tám giao dịch**, theo đúng thứ tự mà các hợp đồng áp đặt:

| # | Giao dịch                            | Lời gọi hợp đồng                                                                   |
| - | ------------------------------------ | ---------------------------------------------------------------------------------- |
| 1 | Triển khai tài sản                   | `new ArcTokenProxy(impl, initData)` trong đó `initData = ArcToken.initialize(...)` |
| 2 | Triển khai mô-đun danh sách cho phép | `new WhitelistRestrictions()`                                                      |
| 3 | Đặt bạn làm quản trị viên của nó     | `whitelist.initialize(issuer)`                                                     |
| 4 | Áp dụng nó cho việc chuyển nhượng    | `token.setRestrictionModule(TRANSFER, whitelist)`                                  |
| 5 | Triển khai mô-đun lợi suất           | `new YieldBlacklistRestrictions()`                                                 |
| 6 | Đặt bạn làm quản trị viên của nó     | `yieldBlacklist.initialize(issuer)`                                                |
| 7 | Áp dụng nó cho lợi suất              | `token.setRestrictionModule(YIELD, yieldBlacklist)`                                |
| 8 | Đăng ký với bộ tạo                   | `factoryV2.registerToken(token, impl)`                                             |

Dạng script chỉ là một lệnh:

```bash
# một lần cho mỗi chuỗi (ops, không phải mỗi token) — triển khai bốn hợp đồng dùng chung
npx tsx deploy-infra.ts

# cho mỗi token — wizard, được phát lại như một script
NAME="Mineral Vault I" SYMBOL=ARCV SUPPLY=1000000 DECIMALS=6 npx tsx create-token.ts
```

Issuer (`PRIVATE_KEY`) ký mọi giao dịch và cuối cùng nắm giữ các vai trò của token (`ArcToken.initialize` cấp cho `msg.sender` — xem bước 7). Việc đăng ký (tx 8) là thứ mở khóa storefront `enableToken` và mọi nâng cấp do factory điều phối.

> **Đừng "tối ưu hóa" nó trở lại thành một khối nguyên khối.** Gom các lời gọi này vào một giao dịch để tiết kiệm lượt khứ hồi sẽ tái tạo đúng lỗi tràn 62 khóa mà đường đi theo từng phần tồn tại để tránh. Cùng giới hạn đó là lý do phân phối lợi suất đi qua từng holder một trên mỗi giao dịch (bước 8). Nếu một lời gọi thất bại với `Quá nhiều tài khoản: N > 62`, thì một giao dịch đang chạm vào quá nhiều tài khoản — hãy tách nó ra, đừng tinh chỉnh nó.

***

## Bước 3 — Chỉ triển khai proxy từ các artifact bị khóa nguồn gốc

`registerToken` là ranh giới an ninh giúp việc triển khai theo từng phần an toàn. Nó chỉ chấp nhận một token mà **runtime codehash khớp với bản chuẩn tắc `ArcTokenProxy`** được nhúng trong factory tại thời điểm triển khai:

```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();
    // …phần triển khai phải nằm trong whitelist của factory, và msg.sender phải nắm giữ
    //    ADMIN_ROLE của token, và token không được đăng ký rồi.
}
```

Vì codehash đó là một **bất biến**, một proxy được tạo từ bất kỳ artifact nào khác sẽ thất bại an toàn với `ProxyCodehashUnknown`. Hai quy tắc theo sau, và cả hai đều được cưỡng chế bởi CI và các quy tắc cứng trong `CLAUDE.md`:

1. **Các token proxy PHẢI được triển khai từ `rome-contracts/out`** — không bao giờ từ `contracts/out`, không bao giờ từ một bản build khác. Đó là bản biên dịch mà codehash chuẩn tắc của factory ghim vào.
2. **`bytecode_hash = "none"` và `cbor_metadata = false`** trên `rome-contracts/foundry.toml` là yếu tố sống còn. Khi metadata được bật, `ArcTokenProxy` runtime *được nhúng* trong factory (`type().runtimeCode`) mang phần đuôi CBOR/IPFS khác với artifact standalone `out/` mà wizard triển khai từ đó — nên on-chain `registerToken` sẽ revert trong khi **mọi bài test Foundry trong source đều pass** (Foundry nhúng cả hai bản sao, nên nó không thấy sự lệch nhau). `rome-contracts/test/ArtifactProvenance.t.sol` ghim thuộc tính này; lần chạy có funding ban đầu chính là thứ đã phát hiện ra nó.

> **Một bộ test trong source màu xanh không chứng minh được nguồn gốc.** Trước khi phát hành một image, và sau BẤT KỲ thay đổi nào về cấu hình compiler, hãy chứng minh thuộc tính này đối với *factory đã triển khai* (chỉ đọc, không dùng khóa):
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> Nó băm artifact cục bộ của bạn `ArcTokenProxy` và xác nhận rằng hash đó xuất hiện nguyên văn bên trong bytecode của factory đã triển khai.

Nếu phải thay đổi cấu hình compiler, **hãy xoay vòng factory** — triển khai một bản mới có immutable ghim codehash mới, rồi nối dây lại — thay vì vá thủ công vào phép kiểm tra:

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

Việc xoay vòng giữ nguyên địa chỉ cũ trong biên nhận; các token đã đăng ký với factory cũ phải đăng ký lại với factory mới.

***

## Bước 4 — Mở một đợt bán qua storefront dùng chung

Việc bán là một hành động riêng với việc tạo, và nó chạy trên storefront dùng chung duy nhất `ArcTokenPurchase` . Ba điều phải đúng trước khi một đợt bán mở, và `ArcTokenPurchase.enableToken` kiểm tra từng cái on-chain:

```solidity
// contracts/src/ArcTokenPurchase.sol
function enableToken(address _tokenContract, uint256 _numberOfTokens, uint256 _tokenPrice)
    external onlyTokenAdmin(_tokenContract)                       // (a) bạn nắm giữ ADMIN_ROLE của token
{
    // (b) token phải được factory biết đến — đây là thứ registerToken (bước 2) đã mua:
    if (ArcTokenFactory(ps.tokenFactory).getTokenImplementation(_tokenContract) == address(0))
        revert TokenNotCreatedByFactory();
    // (c) storefront phải sẵn có hàng tồn để bán:
    if (ArcToken(_tokenContract).balanceOf(address(this)) < _numberOfTokens)
        revert ContractMissingRequiredTokens();
    // …giá và số lượng phải dương.
}
```

Có một yêu cầu ngầm thứ tư mà một tài sản bị khóa buộc phải có: vì **mọi lần chuyển đều đi qua cổng allowlist**, việc chuyển hàng tồn kho sang storefront chỉ thành công nếu chính địa chỉ storefront đã được whitelisted trên token của bạn. Vì vậy trình tự mở bán đầy đủ là:

1. **Whitelisting storefront** trên mô-đun whitelist của token bạn (`batchAddToWhitelist([storefront])` — xem bước 5). Nếu không có điều này, việc chuyển hàng tồn kho ở (2) sẽ revert `TransferRestricted()` một khi đã bị khóa.
2. **Chuyển hàng tồn kho** đến địa chỉ storefront (từ `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** — người mua giờ trả wUSDC và nhận RWA.

Sự phân chia rút tiền đáng biết trên một *storefront* dùng chung, và nó không đối xứng (`arc/roles.ts` là nơi có thẩm quyền):

| Hành động                                                  | Thẩm quyền                                | Được nắm giữ bởi                   |
| ---------------------------------------------------------- | ----------------------------------------- | ---------------------------------- |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | của token `ADMIN_ROLE` (`onlyTokenAdmin`) | bạn, issuer                        |
| `withdrawPurchaseTokens` (phần thu wUSDC)                  | của storefront `DEFAULT_ADMIN_ROLE`       | bất kỳ ai đã triển khai storefront |

Trên một chuỗi tự vận hành, bạn là cả hai. Trên một storefront dùng chung, quỹ thu được là tổng doanh thu của mọi issuer, nên việc rút tiền ở đó là cấp nền tảng — hãy thiết kế quy trình quyết toán xoay quanh điều đó thay vì giả định quyền kiểm soát đơn phương.

***

## Bước 5 — Cắm KYC / tuân thủ của riêng bạn vào

Đây là phần khách hàng đọc trước khi đồng ý, nên nó là mã, không phải văn xuôi. **Tuân thủ được thực thi bởi chính hợp đồng token** — không phải bởi một màn hình off-chain, bộ lọc sequencer, hay chính sách của venue. Các quy tắc đi cùng token tới mọi đường thực thi, trên cả hai làn ví. Thứ chain ghi lại là **kết quả** của quyết định KYC của bạn, dưới dạng một boolean duy nhất:

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol — trạng thái on-chain, đầy đủ
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← một boolean cho mỗi địa chỉ. Không bao giờ có PII.
    bool transfersAllowed;                    // false = bị khóa; chỉ địa chỉ được whitelist mới chuyển được
    EnumerableSet.AddressSet whitelistedAddresses;
}
```

### Giao diện mà quy trình của bạn ghi vào

Quyết định đồng ý/từ chối của nhà cung cấp KYC của bạn trở thành chính xác một trong các lời gọi này:

```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)
}
```

### Ai được phép ghi

> **Thẩm quyền mà một phê duyệt cần là `MANAGER_ROLE` trên&#x20;*****mô-đun*****&#x20;— chứ không phải `ADMIN_ROLE` trên token.** `addToWhitelist` / `batchAddToWhitelist` / `removeFromWhitelist` đều là `onlyRole(MANAGER_ROLE)`, và vai trò đó nằm trên `WhitelistRestrictions` instance. Việc xác thực `ADMIN_ROLE` trên token rồi sau đó ghi vào mô-đun là một lỗi thực sự mà codebase này đã phải trả giá. Đừng đưa `WHITELIST_ADMIN_ROLE` ra làm cách sửa cho một lần từ chối — nó được cấp ở initialize và **không được kiểm tra ở đâu trong suite** (`arc/roles.ts` khẳng định nó là vai trò duy nhất không chặn gì cả).

`WhitelistRestrictions.initialize(issuer)` (tx 3 của wizard) cấp cho issuer `DEFAULT_ADMIN_ROLE`, `ADMIN_ROLE`, `MANAGER_ROLE`, `WHITELIST_ADMIN_ROLE`, và `UPGRADER_ROLE` trên mô-đun đó. Vì vậy issuer nắm `MANAGER_ROLE` ngay từ đầu và cũng có thể ủy quyền nó: vì issuer nắm `DEFAULT_ADMIN_ROLE`, một `grantRole(MANAGER_ROLE, serviceAccount)` trao quyền ghi cho một signer backend.

### Một quyết định trở thành một lần ghi on-chain

Trỏ webhook của nhà cung cấp bạn tới một signer nắm giữ `MANAGER_ROLE`, và khi được duyệt thì batch các địa chỉ:

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

// `signer` phải nắm giữ MANAGER_ROLE trên mô-đun whitelist của CHÍNH token này.
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// nhà cung cấp báo "approved" cho các địa chỉ này:
await wallet.writeContract({
  address: whitelistModule,                 // được resolve TỪ token qua getRestrictionModule
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

Giải quyết `whitelistModule` từ chính token (`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`), không bao giờ từ một bản chiếu được lưu sẵn — nhắm ghi vào sai module là cách mà "approve on any asset" từng ghi vào allowlist của một token.

### Biến thể ký bằng ví (demo tự phục vụ)

Đối với một triển khai thử nghiệm không cần cấp quyền, nơi khách tự đăng ký, hãy cấp `MANAGER_ROLE` cho một `TestApprover` (`rome-contracts/src/TestApprover.sol`) mà `mô-đun` là **bất biến**. Sau đó `approve(address)` có thể được bất kỳ ai gọi, từ chính ví của họ, tự trả gas của họ — nó gọi `addToWhitelist` chỉ khi địa chỉ chưa được liệt kê:

```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);
}
```

Đây là "cổng đăng nhập" — nó cho khách thấy cơ chế thực sự (một phê duyệt trở thành một mục allowlist on-chain mà token sau đó thực thi) mà không lưu ký bất cứ thứ gì. **Đừng bao giờ cấp `MANAGER_ROLE` cho một `TestApprover` trên một token production** — điều đó khiến allowlist của token đó trở nên không cần cấp quyền.

### Khi cổng chặn, và chuyển đổi nhà cung cấp

Cổng chỉ hạn chế khi tài sản đã **được chặn** (`setTransfersAllowed(false)`, `ADMIN_ROLE` trên module). Khi chưa chặn, việc chuyển là mở — hãy xây allowlist trước, rồi mới chặn. Khi đã chặn, một giao dịch chuyển mà một trong hai bên không nằm trong danh sách sẽ revert với lỗi có kiểu `TransferRestricted()` (`0xe827105e`).

> **Sau này chuyển đổi nhà cung cấp KYC không cần thay đổi on-chain.** Module allowlist, interface của nó, và biến boolean nó lưu là không phụ thuộc nhà cung cấp. Để đổi nhà cung cấp, hãy trỏ một nguồn quyết định khác vào cùng `MANAGER_ROLE` signer — không cần triển khai lại, không di chuyển, không đổi trạng thái. Chuỗi không bao giờ biết nhà cung cấp nào đã thực hiện lệnh, chỉ biết kết quả.

***

## Bước 6 — Mở cùng tài sản cho ví Solana

Ba hợp đồng đó cũng có thể truy cập từ một ví Solana **mà không cần bridge và không cần token thứ hai**. Một người dùng native Solana không có khóa EVM ở đâu cả; thay vào đó danh tính EVM của họ là *tổng hợp*, được suy ra on-chain từ signer Solana thực sự của giao dịch:

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

Chương trình Rome EVM suy ra điều này từ chính signer (`do_tx_unsigned::derive_sender`), nên nó không thể bị giả mạo và cũng không tồn tại private key secp256k1 cho nó — **chữ ký Solana là thứ duy nhất có thể điều khiển địa chỉ này**. Phép suy dẫn tham chiếu là `arc/materialise/solana/identity.ts` (`syntheticAddress`), tương thích ở mức byte với quy tắc on-chain:

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

**Việc một builder làm để hỗ trợ làn** — lưu ý rằng *không có phần nào trong đó là thay đổi hợp đồng*:

1. **Đưa người dùng Solana vào whitelist bằng địa chỉ tổng hợp của họ.** Đối với lớp tuân thủ, một địa chỉ tổng hợp là một địa chỉ bình thường: dán pubkey Solana của người dùng, suy ra địa chỉ tổng hợp, rồi `batchAddToWhitelist([synthetic])`. Một allowlist bao trùm cả hai thế giới ví.
2. **Cấp phát PDA ký một lần, trước lệnh đầu tiên có di chuyển giá trị của họ.** PDA có thẩm quyền bên ngoài ký cho một địa chỉ tổng hợp phải tồn tại trước, và việc tạo nó chỉ là một `create_pda` lệnh gọi. Bên nào cũng có thể gửi nó: ví Solana của chính người dùng thông qua lane ( `buildCreatePdaCall`, sau `isSyntheticProvisioned` trả về false), hoặc bên phát hành từ một khóa EVM. Bộ công cụ của bên phát hành Bloom hiện nay gửi nó như một phần của quy trình onboard holder; ứng dụng vẫn chưa làm điều đó ở hành động đầu tiên của người dùng, nên hãy cấp phát trước lần mua đầu tiên của holder. Cách làm với các lời gọi SDK có trong [Ứng dụng hai làn](/vi/huong-dan-cho-nha-phat-trien/dual-lane-app.md#one-time-activate-provision-the-synthetic).
3. **Gửi qua thư viện lane, không làm thủ công.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`yêu cầu proxy của chuỗi soạn giao dịch, và nó xử lý những gì bản dựng thủ công hay làm sai. Mọi lời gọi hợp đồng trên lane đều là một **giao dịch Solana v1 do proxy soạn** (`rome_composeUnsignedAtomicV1`) mà không có address lookup table, và thư viện lane sẽ fail-closed thay vì quay về định dạng cũ hơn. Bước nạp tiền ở bước 5 là một giao dịch Solana riêng biệt do ứng dụng tự xây, không phải một lời gọi hợp đồng được soạn.
   * **Việc phát hiện account và compute budget là nhiệm vụ của proxy.** Nó mô phỏng lời gọi, quyết định quyền ghi của từng account ( `value` là một phần trong những gì nó mô phỏng, nên hãy truyền nó), rồi đặt compute và heap budget cho giao dịch mà nó trả về. Không có gì trong code của bạn tự dựng instruction compute-budget hay thêm fee-pool account.
   * **Mỗi lệnh gọi cần hai chữ ký, cả hai đều từ khóa Solana.** Ví trước tiên ký một thông điệp ủy quyền có thể đọc được (*Rome authorization* cộng với hash của payload EIP-1559 chưa ký; Wallet Standard `solana:signMessage`), mà proxy xác minh trước khi soạn lời gọi. Sau đó nó ký giao dịch v1 mà proxy trả về (`solana:signTransaction` trên wire thô). Thư viện lane xử lý chúng như `signAuthHash` và `signV1Wire`, không bao giờ gộp thành một phương thức duy nhất. Hãy kiểm tra ví của bạn có ký giao dịch v1 trước khi xây dựng trên nó: Solflare hiện đã làm trên devnet, còn Phantom thì chưa.
   * **Ứng dụng tự gửi giao dịch đã ký đến một Solana RPC.** Proxy soạn; nó không phát sóng một lời gọi nguyên tử.
   * **Hãy xác thực trước khi yêu cầu chữ ký.** Thư viện lane phân tích thông điệp đã soạn và từ chối thông điệp sai định dạng trước khi hiện lời nhắc giao dịch.
4. **Hãy cấp quyền wrapper một lần trước lần chi wUSDC đầu tiên.** wUSDC di chuyển token của người dùng với tư cách SPL delegate của người dùng đó, nên một địa chỉ tổng hợp chưa từng được cấp quyền sẽ gặp lỗi SPL `0x4` ở lần chuyển wUSDC đầu tiên. Đọc `isEnabled(user)` trên wrapper và, khi nó là false, hãy chèn trước `approve_spl(wrapper, 2^64-1, mint)` trên Helper precompile như chặng đầu tiên của lần chạy. Đó là một lệnh gọi bình thường trên cả hai lane, một lần cho mỗi ví. Xem [Cấp quyền một lần cho mỗi ví](/vi/cac-khai-niem-cot-loi/token-interop.md#one-time-grant-per-wallet).
5. **Nạp tiền vào trước một lệnh gọi chi tiêu nó.** Khi ở trạng thái nghỉ, địa chỉ tổng hợp không giữ gì cả. Khi nó giữ ít wUSDC hơn số mà lệnh gọi sẽ chi, hãy chuyển phần thiếu từ tài khoản token USDC của chính ví trước (tạo idempotent tài khoản token của địa chỉ tổng hợp, rồi instruction activate-token-account của chương trình Rome, lệnh này chuyển token mà không chạy EVM). Đó là một giao dịch Solana được ký bằng một lời nhắc, và nó được bỏ qua khi địa chỉ tổng hợp đã giữ đủ.
6. **Chỉ relay những gì bộ soạn nguyên tử từ chối.** Một lệnh gọi không vừa với một giao dịch Solana (khoảng 1,4 triệu compute unit, 64 account, 4 KB) sẽ bị bộ soạn từ chối với *kế hoạch nguyên tử bên ngoài yêu cầu soạn lặp*. Chỉ đúng khi nhận được từ chối đó, và không bao giờ trước đó, hãy gọi `rome_relayUnsignedV1` với cùng chữ ký ủy quyền: proxy soạn và gửi nó, trả phí Solana, và phản hồi bằng một hash giao dịch EVM (không phải chữ ký Solana). Địa chỉ tổng hợp trả native gas của chuỗi bằng wei, nên khi số dư của nó thấp hơn `gasLimit x maxFeePerGas`, hãy nạp thêm trước bằng `rome_composeExternalDepositV1` (một giao dịch v1 do ví ký; số tiền phải là bội số của 10^12 wei), trên thực tế một lần cho mỗi ví. Bất kỳ lỗi nào khác, như revert, lỗi mạng hoặc chữ ký bị từ chối, đều được ném lại chứ không relay. Chi tiết ở [Các lệnh gọi được chuyển tiếp và lệnh gọi dài](/vi/huong-dan-cho-nha-phat-trien/call-evm-from-solana.md#relayed-calls-and-long-calls).

Người dùng là **bên trả phí** và trả lamports (SOL) từ ví Solana của họ cho một lời gọi trực tiếp; địa chỉ tổng hợp họ kiểm soát giữ tài sản và không bao giờ giữ SOL. Sau mỗi lần gửi qua lane, hãy thăm dò nonce EVM trước lần tiếp theo — indexer chậm hơn xác nhận Solana một chút, nếu không lời gọi tiếp theo sẽ được dựng trên một nonce mà chuỗi đã tiêu rồi.

> **Nguyên tử trước, relay khi bị từ chối.** Mặc định cho mọi lệnh gọi là một giao dịch EVM được thực thi bên trong một giao dịch Solana, do ví của người dùng ký. Relay là ngoại lệ cho một lệnh gọi không vừa, không phải tuyến thứ hai để chọn theo sở thích: một lời gọi trực tiếp cần hai lời nhắc, một lời gọi relay cần một, và lời gọi relay được thanh toán bằng Rome gas thay vì lamports. Một hành trình nhiều bước (approve, rồi buy) là nhiều lệnh gọi trọn vẹn, mỗi lệnh là một giao dịch riêng, không phải một lần thực thi bị tách qua nhiều giao dịch.

**Vì sao RWA không thể rò rỉ ra ngoài phạm vi.** Tài sản là cư trú trong trạng thái EVM — số dư của nó là storage bên trong các account của chương trình Rome EVM. Không có **SPL mint** của nó và không có gì native Solana để chuyển; các primitive di chuyển tài sản của lane hoạt động trên SPL token account, trong khi số dư storage EVM thì không có. Mọi di chuyển mà chữ ký Solana có thể biểu đạt đều là một lời gọi vào hợp đồng token, vốn chạy cổng allowlist — bộ kiểm thử có vốn xác nhận rằng việc chuyển đến một địa chỉ không nằm trong allowlist sẽ revert y hệt dù được ký bởi khóa EVM hay khóa Solana. Phần tiền mặt (wUSDC) là ngoại lệ có chủ ý: nó được chống đỡ bởi SPL và *không* được quét ra tài khoản token riêng của người dùng.

***

## Bước 7 — Biết ai nắm quyền sau khi triển khai

`ArcToken.initialize` chạy từ constructor của proxy (tx 1) và cấp các vai trò cho `msg.sender` — người điều khiển wizard, tức bên phát hành. Nó cấp **năm** vai trò một cách rõ ràng:

```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);
```

Hai hệ quả mà builder không được bỏ sót:

* **`MINTER_ROLE`, `BURNER_ROLE`, và `UPGRADER_ROLE` KHÔNG được cấp tại initialize.** Nguồn cung ban đầu được mint bên trong `initialize` (một `_mint`, không cần vai trò), nhưng một *sau này* `mint` hoặc `burn` yêu cầu bên phát hành trước tiên tự cấp vai trò cho chính họ. Họ có thể làm vậy, vì họ nắm `DEFAULT_ADMIN_ROLE` (admin của mọi vai trò — không có gì gọi `_setRoleAdmin`, nên `DEFAULT_ADMIN_ROLE` quản trị tất cả chúng). Nhưng đó là một bước bổ sung có chủ ý, không phải một quyền mặc định.
* **`UPGRADER_ROLE` không bao giờ được cấp cho factory.** Nâng cấp thông qua factory (`ArcTokenFactoryV2.upgradeToken`) yêu cầu **chính factory** phải nắm giữ `UPGRADER_ROLE` trên token — cấp nó cho chính mình thì chẳng có tác dụng gì. Việc cấp đó là một cánh cửa một chiều mà factory không thể đảo ngược, nên là opt-in: nếu bạn muốn nâng cấp thông qua factory, `grantRole(UPGRADER_ROLE, factory)` một cách rõ ràng; nếu không, bên phát hành sẽ nâng cấp token trực tiếp.

Mô hình đầy đủ 21 thẩm quyền (11 tên vai trò trải trên sáu hợp đồng — cùng một tên là một thẩm quyền khác nhau trên mỗi hợp đồng) được mã hóa trong `arc/roles.ts`. Hãy gọi `rolesRequiredFor(contract, functionName)` thay vì đọc hợp đồng riêng của một vai trò, vì bốn hàm storefront và cả hai hàm factory đều bị chặn bởi một thẩm quyền nằm trên một *khác* hợp đồng.

***

## Bước 8 — Phân phối lợi nhuận

Token trả lợi nhuận bằng wUSDC (cố định ở `initialize`, có thể thay đổi qua `setYieldToken` dưới `YIELD_MANAGER_ROLE`). Việc phân phối đi qua tập holder:

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

Các cơ chế quan trọng để điều khiển đúng:

* **Toàn bộ `totalAmount` chỉ được rút từ người gọi ở `startIndex == 0` cửa sổ** (`safeTransferFrom(msg.sender, this, totalAmount)`). Hãy approve tổng đó cho token trước cửa sổ đầu tiên; các cửa sổ sau sẽ chi từ số dư đã được giữ sẵn.
* **`nextIndex` quay về `0` khi việc duyệt hoàn tất.** Lặp `distributeYieldWithLimit(total, nextIndex, …)` cho đến khi `nextIndex` quay lại `0`.
* **Phần của các holder bị hạn chế nhận lợi nhuận vẫn nằm trong hợp đồng token** — họ bị bỏ qua, không được phân phối lại.

> **Trên Rome, trần thực tế là `maxHolders = 1` mỗi giao dịch** — cùng giới hạn 62 account từ bước 2 — nên lợi nhuận được duyệt một holder mỗi giao dịch (đo được khoảng 318 K gas mỗi holder). Mọi holder trong whitelist, dù là EVM hay native Solana, đều nhận theo tỷ lệ; script tham chiếu là `measure-yield-batch.ts` và bước yield của smoke có vốn xác nhận nó từ đầu đến cuối.

```bash
# Mẫu tham chiếu USAGE.md: đi qua một holder mỗi tx cho đến khi nextIndex quay về 0
distributeYieldWithLimit(total, offset, 1)
```

***

## Bước 9 — Xác minh những gì bạn đã triển khai

Không có gì tự động nộp source ở đâu trong Rome, nên xác minh là một bước bạn tự chạy. Sau bất kỳ lần triển khai nào — hạ tầng hay token mới — hãy chạy trình xác minh chỉ đọc, không cần khóa, idempotent (các hợp đồng đã được xác minh sẽ bị bỏ qua, nên chạy lại sau khi phát hành một token thì chỉ xác minh các hợp đồng mới):

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

Nó kỳ vọng **ba** các hợp đồng cho mỗi token (proxy + cả hai module) cộng với hạ tầng, và báo cáo những gì nó không thể xác minh thay vì làm deploy thất bại. Làm thủ công việc này một lần đã bỏ sót bảy hợp đồng.

Đăng ký tài sản vào Rome registry xem việc xác minh như một **cổng**, không phải trang trí (`register-asset.ts`): một token nhận được `apps/arc/<chain>.json` bản ghi — mang theo `standard: arc-permissioned-erc20` và một `transferRestriction` đặt tên cho cổng allowlist của nó — chỉ khi cả ba hợp đồng của nó được xác minh trên Sourcify. Bản ghi đó đọc dữ liệu từ chuỗi để lấy sự thật (một token được triển khai bằng wizard không có receipt được commit) và chỉ ghi một bản checkout registry cục bộ; việc đưa PR vào là việc của con người.

```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
```

Để có bằng chứng đầu-cuối cho toàn bộ vòng lặp — tạo, whitelist, chặn, bán trên cả hai lane, phân phối lợi nhuận — hãy chạy smoke có vốn trên bất kỳ chain nào:

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

***

## Bước 10 — Biết điều gì ở ngoài chuỗi

Mô hình niềm tin được thiết kế để trung thực, và builder nên trình bày nó theo cùng cách đó với người dùng của họ:

* **Cờ allowlist là quyết định ngoài chuỗi của bạn.** KYC/AML diễn ra trong quy trình của bạn — nhà cung cấp của bạn, quy tắc của bạn. Chuỗi chỉ ghi và thực thi *kết quả*, `isWhitelisted(addr)`.
* **Trạng thái on-chain duy nhất là một boolean cho mỗi địa chỉ.** Không có PII, không có tài liệu, không có tuyên bố danh tính, không có xác nhận on-chain, không có bằng chứng mật mã về địa chỉ thuộc về ai. Đây là mô hình của Arc như được dùng trên Plume; nó đánh đổi các chuyển giao nặng nề của một chuẩn danh tính on-chain (ví dụ ERC-3643) lấy cơ chế nhẹ hơn và niềm tin do bên phát hành nắm giữ.
* **Khôi phục là quyền của bên phát hành, không phải cửa thoát.** Một ví bị mất — EVM hay Solana — được khôi phục bằng cách whitelist một địa chỉ thay thế và, nếu cần, dùng mint/burn/upgrade theo quy trình pháp lý của riêng bạn (Arc không có primitive forced-transfer tích hợp sẵn). Vì vậy việc lưu giữ khóa của bên phát hành là một phần trong tư thế tuân thủ.
* **Không có relayer riêng của Bloom, không có bề mặt lưu ký.** Người dùng giữ gas của riêng họ trên cả hai lane. Một dịch vụ fee-payer do ứng dụng vận hành sẽ đồng nghĩa với việc giữ khóa — một bề mặt mà sản phẩm RWA không nên thêm vào. Lần relay duy nhất trên lane Solana là của proxy chuỗi, dành cho một lời gọi quá lớn cho một giao dịch, và người dùng vẫn ủy quyền nó bằng chính chữ ký của họ và trả bằng Rome gas của chính họ.

***

## Tham khảo: bộ công cụ

Mọi script đều chạy từ `scripts/` thư mục của bộ công cụ, với `PRIVATE_KEY`, `CHAIN_ID`, và `REGISTRY_ROOT` được export.

| Script                          | Nó làm gì                                                                         |
| ------------------------------- | --------------------------------------------------------------------------------- |
| `deploy-infra.ts`               | Một lần cho mỗi chain: bốn hợp đồng dùng chung, được cấu hình cho wUSDC.          |
| `create-token.ts`               | Wizard theo từng phần — tám giao dịch, một token.                                 |
| `rotate-factory.ts`             | Triển khai một factory mới sau thay đổi compiler/artifact; nối lại.               |
| `verify-artifact-provenance.ts` | Chứng minh artifact proxy của bạn được factory đã triển khai chấp nhận (chỉ đọc). |
| `verify-deployments.ts`         | Xác minh cả ba hợp đồng cho mỗi token + hạ tầng trên Sourcify (chỉ đọc).          |
| `register-asset.ts`             | Xác minh cổng, rồi chuẩn bị bản ghi tuân thủ của registry.                        |
| `measure-yield-batch.ts`        | Đo lại trần lợi nhuận mỗi tx trên một chain.                                      |
| `smoke.ts`                      | Bộ kiểm thử đầu-cuối có vốn trên cả hai lane.                                     |
| `sweep-synthetic-native.ts`     | Thu hồi native gas từ một tài khoản tổng hợp (vệ sinh kiểm thử).                  |

Phần nền sâu hơn nằm cùng với hướng dẫn này: `docs/ARCHITECTURE.md` (mô hình ứng dụng nhiều lớp), `docs/LANES.md` (hai lane và lưu ký), `docs/COMPLIANCE.md` (mô hình niềm tin), và `docs/USAGE.md` (runbook cho operator).


---

# 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/ung-dung-tren-rome/bloom/building.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.
