> 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/aplikasi-di-rome/bloom/building.md).

# Membangun di Bloom

Panduan ini menjawab satu pertanyaan: **bagaimana seorang builder melakukannya sendiri?** Ambil kontrak aset dunia nyata Solidity, jalankan di chain Rome sebagai token berizin, buka aksesnya ke dompet Solana maupun dompet EVM tanpa bridge dan tanpa token kedua, dan sambungkan keputusan KYC/kepatuhan Anda sendiri.

Audiensnya adalah developer yang mengevaluasi atau mengadopsi pendekatan ini. Bloom adalah contoh kerja yang digunakan sepanjang panduan — aset live-nya **ARCV ("Mineral Vault I")** di Hadrian (`200010`) adalah contoh nyata dari setiap langkah di bawah ini, dan tanda terima deployment-nya (`deployments/200010.tokens/ARCV.json`) mencatat tiga kontrak dan delapan transaksi persis yang dihasilkan metode ini.

Tidak ada di sini yang memodifikasi kontrak aset. Bloom menjalankan framework Arc milik Plume **tanpa modifikasi** (pohon vendored di `contracts/`, identik byte demi byte dengan `plumenetwork/contracts` pada pin di `NOTICE`); satu-satunya kontrak di sisi Rome adalah `ArcTokenFactoryV2`, yang menambahkan satu fungsi. Pekerjaannya ada pada *bagaimana* Anda mendeploy dan *bagaimana* Anda mengendalikan kontrak-kontrak itu — bukan mengubahnya.

> **Metodenya, sekilas.** Token berizin adalah **tiga** kontrak yang dideploy. Itu dideploy **secara bertahap** — satu kontrak per transaksi — karena panggilan factory satu transaksi melebihi batas akun per transaksi di Rome. Proxy-nya harus berasal dari **artefak yang terkunci provenance** atau registrasi on-chain gagal tertutup. Kepatuhan adalah satu **boolean per alamat**, ditulis oleh siapa pun yang diotorisasi proses KYC Anda. Dan deployment yang sama dapat diakses dari dompet Solana melalui **pengirim sintetis** yang diturunkan di chain — tanpa bridge, tanpa wrapped token, tanpa allowlist kedua.

## Prasyarat

Semua alat referensi berada di `scripts/` (jalankan `npm install` di sana terlebih dahulu). Setiap perintah mengambil fakta chain dari registry, bukan meng-hardcode-nya, jadi mengalihkan ke chain Rome lain adalah perubahan environment, bukan perubahan kode:

```bash
export PRIVATE_KEY=…       # kunci deployer/issuer yang didanai di chain target
export CHAIN_ID=200010     # memilih chain
export REGISTRY_ROOT=…     # checkout dari registry chain Rome
# Alur jalur Solana juga membutuhkan:
export SOLANA_KEYPAIR=…    # path ke JSON keypair Solana yang didanai (pembayar biaya)
```

Gas pada chain Rome didenominasikan dalam USDC; rangkaian deployment penuh biayanya sekitar 10–15 unit native di devnet. Kunci hanya berasal dari environment — jangan pernah dari repo. Build kontrak sekali sebelum mendeploy, karena artefak **harus** berasal dari `rome-contracts/out` (lihat langkah 3):

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

***

## Langkah 1 — Pahami apa itu token berizin *adalah* di sini

Token Bloom berizin bukan satu kontrak. Itu adalah **tiga kontrak yang dideploy**, plus empat kontrak bersama yang dideploy ops sekali per chain dan dipakai ulang oleh setiap issuer.

**Tiga kontrak per token** — satu aset memiliki tepat ini:

| Kontrak                      | Sumber                                                      | Apa itu                                                                                                                     |
| ---------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | Token itu sendiri — proxy UUPS yang menunjuk ke `ArcToken` implementasi bersama. Saldo, pemegang, dan peran berada di sini. |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | Modul allowlist. Satu boolean per alamat; ditegakkan pada setiap transfer saat aset digated.                                |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | Modul yield. Mengecualikan alamat dari distribusi yield tanpa menyentuh kepemilikannya.                                     |

**Empat kontrak bersama, satu per chain** (dideploy oleh `deploy-infra.ts`):

| Kontrak                   | Peran                                                                              |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `RestrictionsRouter`      | Registry tipe modul — wiring chain, diatur sekali oleh ops.                        |
| `ArcTokenFactoryV2`       | Registrasi + codehash proxy kanonik (langkah 2–3).                                 |
| `ArcToken` (implementasi) | Logika aset bersama yang dapat digunakan ulang yang diproksikan oleh setiap token. |
| `ArcTokenPurchase`        | Satu storefront, dibagi oleh setiap token dan issuer (langkah 4).                  |
| wUSDC                     | Sisi kas — mata uang yield dan mata uang jual-beli.                                |

Pembatasan transfer dan pembatasan yield adalah **modul yang sengaja dipisahkan**: seorang pemegang dapat dilarang menerima pendapatan sambil tetap memegang aset, seperti itulah sanksi dan perintah pengadilan memang bekerja.

Fakta "tiga kontrak" ini sangat penting bagi verifikasi. `verify-deployments.ts` dan `register-asset.ts` keduanya mengharapkan token mengarah ke tiga kontrak yang dideploy — verifikasi manual sekali melewatkan tujuh di antaranya di seluruh chain (setiap modul yield dan tiga modul whitelist).

Siklus hidupnya memiliki tiga fase dan satu jalan satu arah:

```
draft     proxy aktif, tidak ada modul terhubung — tidak ada yang meng-gate transfer
  ↓
ungated   modul terhubung, transfersAllowed = true   (WhitelistRestrictions.initialize menetapkan ini)
  ↓  cetak suplai, bangun allowlist               [dapat dibalik]
gated     setTransfersAllowed(false)                [PINTU SATU ARAH — suplai menjadi final]
```

Penggatingan bersifat satu arah untuk suplai karena mint dan burn melewati `address(0)` sebagai lawan transaksi, dan `address(0)` tidak pernah bisa masuk allowlist — jadi setelah digated, suplai baru tidak akan pernah bisa dicetak. Itulah jaminan yang diberikan aset berizin kepada para pemegangnya.

***

## Langkah 2 — Deploy secara bertahap, jangan pernah sebagai satu transaksi

Arc menyediakan satu kali jalan `createToken` yang mendeploy proxy dan kedua modul serta menyambungkannya dalam satu panggilan. **Di Rome Anda tidak bisa menggunakannya.** Transaksi itu membutuhkan **77 account lock Solana**, dan batas per transaksi Rome adalah **62 lock**. Itu tidak akan pernah muat — tidak dengan penyesuaian, tidak dengan pass optimisasi.

Jadi deployment-nya adalah **secara bertahap**: setiap kontrak dalam transaksinya sendiri, dan token memperoleh status setara factory melalui `ArcTokenFactoryV2.registerToken` alih-alih monolitik `createToken`. Ini adalah wizard pembuatan, dan itulah yang dilakukan konsol issuer langkah demi langkah:

![wizard pembuatan](/files/f97c0d2bfa9130b1a03f29d292b800c7cdd7626b)

Implementasi referensinya adalah `arc/plan/create.ts` (`createSequence()`), yang dirender aplikasi sebagai enam kartu (`bloom/lib/createCards.ts`) di atas **delapan transaksi**, sesuai urutan persis yang dipaksakan oleh kontrak:

| # | Transaksi                              | Panggilan kontrak                                                                 |
| - | -------------------------------------- | --------------------------------------------------------------------------------- |
| 1 | Deploy aset                            | `new ArcTokenProxy(impl, initData)` di mana `initData = ArcToken.initialize(...)` |
| 2 | Deploy modul allowlist                 | `new WhitelistRestrictions()`                                                     |
| 3 | Tetapkan Anda sebagai administratornya | `whitelist.initialize(issuer)`                                                    |
| 4 | Tegakkan pada transfer                 | `token.setRestrictionModule(TRANSFER, whitelist)`                                 |
| 5 | Deploy modul yield                     | `new YieldBlacklistRestrictions()`                                                |
| 6 | Tetapkan Anda sebagai administratornya | `yieldBlacklist.initialize(issuer)`                                               |
| 7 | Tegakkan pada yield                    | `token.setRestrictionModule(YIELD, yieldBlacklist)`                               |
| 8 | Daftarkan ke factory                   | `factoryV2.registerToken(token, impl)`                                            |

Bentuk skripnya adalah satu perintah:

```bash
# sekali per chain (ops, bukan per token) — mendeploy empat kontrak bersama
npx tsx deploy-infra.ts

# per token — wizardnya, diputar ulang sebagai skrip
NAME="Mineral Vault I" SYMBOL=ARCV SUPPLY=1000000 DECIMALS=6 npx tsx create-token.ts
```

Issuer (`PRIVATE_KEY`) menandatangani setiap transaksi dan akhirnya memegang peran token (`ArcToken.initialize` memberikan kepada `msg.sender` — lihat langkah 7). Registrasi (tx 8) adalah yang membuka `enableToken` dan setiap upgrade yang dimediasi factory.

> **Jangan "mengoptimalkan" ini kembali menjadi monolit.** Menggabungkan panggilan ini ke dalam satu transaksi untuk menghemat round-trip justru memperkenalkan kembali overflow 62-lock yang ingin dihindari jalur bertahap. Batas yang sama itulah alasan distribusi yield berjalan satu pemegang per transaksi (langkah 8). Jika sebuah panggilan gagal dengan `Terlalu banyak akun: N > 62`, satu transaksi menyentuh terlalu banyak akun — pecah saja, jangan disetel.

***

## Langkah 3 — Deploy proxy hanya dari artefak yang terkunci provenance

`registerToken` adalah batas keamanan yang membuat deployment bertahap aman. Ini hanya akan menerima token yang **runtime codehash-nya cocok dengan yang kanonik `ArcTokenProxy`** yang ditanam ke dalam factory saat deployment:

```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();
    // …implementasi harus ada di whitelist factory, dan msg.sender harus memegang
    //    ADMIN_ROLE token, dan token belum boleh terdaftar.
}
```

Karena codehash itu adalah **immutable**, proxy yang dibangun dari artefak lain mana pun gagal tertutup dengan `ProxyCodehashUnknown`. Dua aturan berikut berlaku, dan keduanya ditegakkan oleh CI dan aturan keras dalam `CLAUDE.md`:

1. **Proxy token HARUS dideploy dari `rome-contracts/out`** — jangan pernah dari `contracts/out`, jangan pernah dari build lain. Itulah kompilasi yang dipin oleh codehash kanonik factory.
2. **`bytecode_hash = "none"` dan `cbor_metadata = false`** di `rome-contracts/foundry.toml` sangat penting. Dengan metadata diaktifkan, runtime `ArcTokenProxy` runtime *yang tertanam* di factory (`type().runtimeCode`) membawa ekor CBOR/IPFS yang berbeda dari artefak mandiri `out/` yang menjadi sumber deployment wizard — jadi di chain `registerToken` melakukan revert sementara **setiap tes Foundry dalam source lulus** (Foundry menyematkan kedua salinan, jadi tidak bisa melihat ketidakcocokan). `rome-contracts/test/ArtifactProvenance.t.sol` mengunci properti ini; run yang didanai-lah yang awalnya menangkapnya.

> **Suite in-source yang hijau tidak membuktikan provenance.** Sebelum mengirim image, dan setelah perubahan setting compiler APAPUN, buktikan properti ini terhadap *yang dideploy* factory (read-only, tanpa kunci):
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> Ini menghitung hash atas `ArcTokenProxy` artefak lokal Anda dan memastikan hash itu muncul persis di dalam bytecode factory yang dideploy.

Jika setting compiler harus diubah, **rotasikan factory** — deploy yang baru dengan immutable yang mem-pin codehash baru, dan lakukan rewiring — alih-alih mem-patch pengecekan secara manual:

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

Rotasi mempertahankan alamat lama dalam receipt; token yang terdaftar pada factory lama harus mendaftar ulang pada factory baru.

***

## Langkah 4 — Buka penjualan melalui storefront bersama

Menjual adalah tindakan terpisah dari membuat, dan berjalan pada satu `ArcTokenPurchase` storefront bersama. Tiga hal harus benar sebelum penjualan dibuka, dan `ArcTokenPurchase.enableToken` memeriksa masing-masing di chain:

```solidity
// contracts/src/ArcTokenPurchase.sol
function enableToken(address _tokenContract, uint256 _numberOfTokens, uint256 _tokenPrice)
    external onlyTokenAdmin(_tokenContract)                       // (a) Anda memegang ADMIN_ROLE token
{
    // (b) token harus dikenal factory — inilah yang dibeli oleh registerToken (langkah 2):
    if (ArcTokenFactory(ps.tokenFactory).getTokenImplementation(_tokenContract) == address(0))
        revert TokenNotCreatedByFactory();
    // (c) storefront harus sudah memegang inventaris penjualan:
    if (ArcToken(_tokenContract).balanceOf(address(this)) < _numberOfTokens)
        revert ContractMissingRequiredTokens();
    // …harga dan jumlah harus positif.
}
```

Ada persyaratan keempat yang tersirat yang dipaksakan oleh aset yang digate: karena **setiap transfer melewati gate allowlist**, memindahkan inventaris ke storefront hanya berhasil jika alamat storefront itu sendiri ada di whitelist token Anda. Jadi urutan lengkap membuka penjualan adalah:

1. **Masukkan storefront ke whitelist** pada modul whitelist token Anda (`batchAddToWhitelist([storefront])` — lihat langkah 5). Tanpa ini, transfer inventaris pada (2) akan revert `TransferRestricted()` setelah digate.
2. **Transfer inventaris** ke alamat storefront (dari receipt infra `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** — pembeli kini membayar wUSDC dan menerima RWA.

Pembagian penarikan layak diketahui pada *storefront* bersama, dan ini tidak simetris (`arc/roles.ts` adalah otoritasnya):

| Tindakan                                                   | Otoritas                              | Dimiliki oleh                       |
| ---------------------------------------------------------- | ------------------------------------- | ----------------------------------- |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | token `ADMIN_ROLE` (`onlyTokenAdmin`) | Anda, issuer-nya                    |
| `withdrawPurchaseTokens` (hasil wUSDC)                     | storefront `DEFAULT_ADMIN_ROLE`       | siapa pun yang mendeploy storefront |

Pada chain self-hosted Anda adalah keduanya. Pada storefront bersama, kumpulan hasil adalah pendapatan semua issuer, jadi penarikannya berada di level platform — rancang settlement Anda berdasarkan itu, bukan dengan mengasumsikan kontrol sepihak.

***

## Langkah 5 — Sambungkan KYC / kepatuhan Anda sendiri

Ini adalah bagian yang dibaca pelanggan sebelum setuju, jadi ini kode, bukan prosa. **Kepatuhan ditegakkan oleh kontrak token itu sendiri** — bukan oleh penyaringan off-chain, filter sequencer, atau kebijakan venue. Aturan-aturan itu ikut bersama token ke jalur eksekusi apa pun, pada kedua jalur dompet. Yang dicatat chain adalah **hasil** dari keputusan KYC Anda, sebagai satu boolean:

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol — state on-chain, secara lengkap
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← satu boolean per alamat. Tidak ada PII, sama sekali.
    bool transfersAllowed;                    // false = gated; hanya alamat yang di-whitelist yang bisa transfer
    EnumerableSet.AddressSet whitelistedAddresses;
}
```

### Interface yang ditulis oleh proses Anda

Keputusan setuju/tolak dari vendor KYC Anda menjadi tepat salah satu panggilan ini:

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

### Siapa yang boleh menulis

> **Otoritas yang dibutuhkan sebuah persetujuan adalah `MANAGER_ROLE` pada&#x20;*****modul*****&#x20;— bukan `ADMIN_ROLE` pada token.** `addToWhitelist` / `batchAddToWhitelist` / `removeFromWhitelist` semuanya `onlyRole(MANAGER_ROLE)`, dan peran itu berada pada `WhitelistRestrictions` Mengautentikasi `ADMIN_ROLE` pada token lalu menulis ke modul adalah cacat nyata yang sudah pernah harus ditanggung codebase ini. Jangan tawarkan `WHITELIST_ADMIN_ROLE` sebagai perbaikan atas penolakan — itu diberikan saat initialize dan **tidak diperiksa di mana pun dalam suite** (`arc/roles.ts` menegaskan bahwa itulah satu-satunya peran yang tidak mengunci apa pun).

`WhitelistRestrictions.initialize(issuer)` (tx 3 dari wizard) memberikan issuer `DEFAULT_ADMIN_ROLE`, `ADMIN_ROLE`, `MANAGER_ROLE`, `WHITELIST_ADMIN_ROLE`, dan `UPGRADER_ROLE` pada modul itu. Jadi issuer memegang `MANAGER_ROLE` secara bawaan dan juga dapat mendelegasikannya: karena issuer memegang `DEFAULT_ADMIN_ROLE`, sebuah `grantRole(MANAGER_ROLE, serviceAccount)` menyerahkan penulisan ke penanda tangan backend.

### Sebuah keputusan menjadi penulisan on-chain

Arahkan webhook vendor Anda ke penanda tangan yang memegang `MANAGER_ROLE`, dan saat ada approve, batch alamat-alamatnya:

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

// `signer` harus memegang MANAGER_ROLE pada modul whitelist token INI.
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// vendor berkata "disetujui" untuk alamat-alamat ini:
await wallet.writeContract({
  address: whitelistModule,                 // resolved DARI token via getRestrictionModule
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

Tentukan `whitelistModule` dari token itu sendiri (`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`), bukan dari proyeksi yang di-cache — mengarahkan penulisan ke modul yang salah adalah bagaimana "approve pada aset apa pun" pernah menulis ke allowlist satu token.

### Varian yang ditandatangani wallet (demo swadaya)

Untuk deployment uji tanpa izin tempat pengunjung mendaftarkan diri sendiri, berikan `MANAGER_ROLE` kepada sebuah `TestApprover` (`rome-contracts/src/TestApprover.sol`) yang `modul` adalah **immutable**. Lalu `approve(address)` dapat dipanggil oleh siapa pun, dari wallet mereka sendiri, membayar gas mereka sendiri — ini memanggil `addToWhitelist` hanya jika alamat tersebut belum terdaftar:

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

Ini adalah "gerbang masuk" — ini menunjukkan kepada pengunjung mekanisme sebenarnya (sebuah approval menjadi entri allowlist on-chain yang kemudian ditegakkan token) tanpa mengasuh apa pun. **Jangan pernah berikan `MANAGER_ROLE` kepada sebuah `TestApprover` pada token produksi** — ini membuat allowlist token itu tanpa izin.

### Saat gerbang mulai berlaku, dan mengganti penyedia

Gerbang hanya membatasi setelah aset **digate** (`setTransfersAllowed(false)`, `ADMIN_ROLE` pada modul). Selama belum digate, transfer terbuka — bangun allowlist Anda dulu, lalu gate. Setelah digate, transfer di mana salah satu pihak tidak ada di daftar akan revert dengan error bertipe `TransferRestricted()` (`0xe827105e`).

> **Mengganti penyedia KYC di kemudian hari tidak memerlukan perubahan on-chain.** Modul allowlist, antarmukanya, dan boolean yang disimpannya bersifat agnostik terhadap penyedia. Untuk berganti vendor, arahkan sumber keputusan yang berbeda ke `MANAGER_ROLE` penanda tangan yang sama — tanpa redeploy, tanpa migrasi, tanpa perubahan state. Chain tidak pernah tahu vendor mana yang membuat panggilan, hanya hasilnya.

***

## Langkah 6 — Buka aset yang sama untuk wallet Solana

Tiga kontrak yang sama dapat dijangkau dari wallet Solana **tanpa bridge dan tanpa token kedua**. Pengguna native Solana tidak memiliki kunci EVM di mana pun; sebagai gantinya identitas EVM mereka adalah *sintetis*, diturunkan on-chain dari penanda tangan Solana yang sebenarnya dari transaksi:

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

Program Rome EVM menurunkan ini dari penanda tangan itu sendiri (`do_tx_unsigned::derive_sender`), jadi ini tidak bisa dipalsukan dan tidak ada kunci privat secp256k1 untuknya — **tanda tangan Solana adalah satu-satunya hal yang dapat mengendalikan alamat ini**. Derivasi referensinya ada di `arc/materialise/solana/identity.ts` (`syntheticAddress`), kompatibel byte-wise dengan aturan on-chain:

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

**Apa yang dilakukan builder untuk mendukung jalur ini** — perhatikan bahwa *tidak satu pun dari ini merupakan perubahan kontrak*:

1. **Whitelist pengguna Solana berdasarkan alamat sintetis mereka.** Bagi lapisan kepatuhan, alamat sintetis adalah alamat biasa: tempel pubkey Solana pengguna, turunkan sintetisnya, dan `batchAddToWhitelist([synthetic])`. Satu allowlist mencakup kedua dunia wallet.
2. **Sediakan signing PDA sekali, sebelum transaksi pertama mereka.** PDA otoritas eksternal yang mengesahkan tanda tangan sintetis harus sudah ada terlebih dahulu, dan membuatnya adalah panggilan jalur EVM (`create_pda`) — pengguna yang hanya memakai Solana tidak dapat melakukannya sendiri, jadi issuer yang menyediakannya. Penyediaan adalah `external_auth`, bukan lazy.
3. **Kirim melalui pustaka lane, bukan secara manual.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`) membangun sebuah `DoTxUnsigned` instruksi — sebuah *tanpa tanda tangan* payload EIP-1559 yang diotorisasi oleh tanda tangan Solana — dan menangani empat hal yang sering salah pada transaksi buatan tangan:
   * **Penemuan akun** melalui `rome_emulateCallAccounts`, yang menentukan dapat-tidaknya setiap akun ditulis. Nilai `value` **harus** harus diteruskan ke discovery, atau transfer value menandai PDA saldo penerima sebagai read-only dan gagal on-chain.
   * **Anggaran komputasi** — sebuah `DoTxUnsigned` membutuhkan **heap frame 250 KB** dan **1,35 juta CU** batas (EVM Rome melampaui default 32 KB / 200 K); menyisakan headroom sekitar 50 K di bawah batas 1,4 M Solana.
   * **PDA wallet kas (fee)**, ditambahkan sebagai writable — discovery tidak menyertakannya.
   * **Fallback v0 + lookup-table.** Saat akun sebuah panggilan melampaui envelope transaksi legacy 1232 byte, klien mengirim ulang sebagai transaksi v0 melalui address lookup table baru. (Diukur hari ini, pembelian storefront muat dalam envelope legacy pada \~1.061 dari 1232 byte; pemegang baru yang associated token account-nya masih harus dibuat bisa melampauinya.)

Pengguna adalah **pembayar fee** dan membayar lamports (SOL) dari wallet Solana mereka; sintetis yang mereka kendalikan memegang aset dan tidak pernah memegang SOL. Setelah setiap pengiriman lane, polling nonce EVM sebelum yang berikutnya — indexer sedikit tertinggal dari konfirmasi Solana.

> **Jalur ini HANYA ATOMIK.** Sebuah `DoTxUnsigned` adalah satu transaksi EVM yang dieksekusi di dalam satu transaksi Solana oleh atomic VM Rome. Iterative VM (satu eksekusi EVM yang di-staging di beberapa transaksi Solana) adalah **belum dapat dijangkau melalui jalur ini**. Panggilan yang tidak muat secara atomik sama sekali tidak bisa memakai jalur ini — perbaikannya adalah panggilan yang lebih kecil, bukan fallback. Perhatikan bahwa fallback v0+ALT adalah *envelope*, bukan eksekusi yang berbeda; itu tetap satu transaksi atomik.

**Mengapa RWA tidak bisa bocor keluar dari perimeter.** Aset ini berada sebagai state EVM — saldonya adalah storage di dalam akun program EVM Rome. Tidak ada **mint SPL** darinya dan tidak ada sesuatu native Solana untuk dipindahkan; primitive pemindahan aset pada jalur ini beroperasi pada akun token SPL, sedangkan saldo storage EVM tidak memilikinya. Setiap perpindahan yang bisa diekspresikan tanda tangan Solana adalah panggilan ke kontrak token, yang menjalankan gerbang allowlist — suite yang didanai menegaskan bahwa transfer ke alamat yang tidak di-whitelist revert identik baik ditandatangani dengan kunci EVM maupun kunci Solana. Jalur kas (wUSDC) adalah pengecualian yang disengaja: ia didukung SPL dan *memang* disweep keluar ke akun token milik pengguna sendiri.

***

## Langkah 7 — Ketahui siapa yang memegang otoritas setelah deploy

`ArcToken.initialize` berjalan dari konstruktor proxy (tx 1) dan memberikan peran kepada `msg.sender` — driver wizard, yaitu issuer. Ini memberikan **lima** peran secara eksplisit:

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

Dua konsekuensi yang tidak boleh terlewat oleh builder:

* **`MINTER_ROLE`, `BURNER_ROLE`, dan `UPGRADER_ROLE` TIDAK diberikan saat initialize.** Pasokan awal dicetak di dalam `initialize` (sebuah internal `_mint`, tidak perlu peran), tetapi *kemudian* `mint` atau `burn` mengharuskan issuer terlebih dahulu memberikan peran itu kepada diri mereka sendiri. Mereka bisa, karena mereka memegang `DEFAULT_ADMIN_ROLE` (admin dari setiap peran — tidak ada yang memanggil `_setRoleAdmin`, jadi `DEFAULT_ADMIN_ROLE` mengadministrasikan semuanya). Tetapi itu adalah langkah ekstra yang disengaja, bukan kekuatan yang otomatis tersedia.
* **`UPGRADER_ROLE` tidak pernah diberikan kepada factory.** Peningkatan yang dimediasi factory (`ArcTokenFactoryV2.upgradeToken`) memerlukan **factory itu sendiri** untuk memegang `UPGRADER_ROLE` pada token — memberikannya kepada diri sendiri tidak berpengaruh. Pemberian itu adalah pintu satu arah yang tidak bisa dibalik oleh factory, jadi sifatnya opt-in: jika Anda ingin peningkatan yang dimediasi factory, `grantRole(UPGRADER_ROLE, factory)` secara eksplisit; jika tidak, issuer yang meningkatkan token secara langsung.

Model 21-otoritas lengkap (11 nama peran di enam kontrak — nama yang sama adalah otoritas berbeda di tiap kontrak) dikodifikasi dalam `arc/roles.ts`. Tanyakan `rolesRequiredFor(contract, functionName)` daripada membaca kontrak milik peran itu sendiri, karena empat fungsi storefront dan kedua fungsi factory dikunci oleh otoritas yang berada pada *kontrak yang berbeda* .

***

## Langkah 8 — Distribusikan yield

Token membayar yield dalam wUSDC (ditetapkan pada `initialize`, dapat diubah melalui `setYieldToken` di bawah `YIELD_MANAGER_ROLE`). Distribusi menelusuri himpunan holder:

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

Mekanisme yang penting untuk menjalankannya dengan benar:

* **Seluruh `totalAmount` diambil dari pemanggil hanya pada `startIndex == 0` jendela** (`safeTransferFrom(msg.sender, this, totalAmount)`). Approve total itu ke token sebelum jendela pertama; jendela berikutnya membayar dari saldo yang sudah dipegang.
* **`nextIndex` berputar ke `0` saat penelusuran selesai.** Loop `distributeYieldWithLimit(total, nextIndex, …)` sampai `nextIndex` kembali `0`.
* **Bagian holder yang dibatasi yield tetap berada di kontrak token** — mereka dilewati, bukan didistribusikan ulang.

> **Di Rome batas praktisnya adalah `maxHolders = 1` per transaksi** — batas 62 akun yang sama dari langkah 2 — jadi yield ditelusuri satu holder per transaksi (terukur \~318 K gas per holder). Setiap holder yang di-whitelist, baik EVM maupun native Solana, menerima secara proporsional; skrip referensinya adalah `measure-yield-batch.ts` dan langkah yield pada smoke yang didanai menegaskannya end-to-end.

```bash
# Bentuk referensi USAGE.md: telusuri satu holder per tx sampai nextIndex berputar ke 0
distributeYieldWithLimit(total, offset, 1)
```

***

## Langkah 9 — Verifikasi apa yang Anda deploy

Tidak ada yang secara otomatis mengirim source ke mana pun di Rome, jadi verifikasi adalah langkah yang Anda jalankan. Setelah deploy apa pun — infrastruktur atau token baru — jalankan verifier yang read-only, tanpa kunci, dan idempoten (kontrak yang sudah terverifikasi dilewati, jadi menjalankannya ulang setelah menerbitkan token hanya memverifikasi yang baru):

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

Ia mengharapkan **tiga** kontrak per token (proxy + kedua modul) plus infrastrukturnya, dan melaporkan apa yang tidak bisa diverifikasi alih-alih membuat deploy gagal. Melakukan ini secara manual sekali membuat tujuh kontrak terlewat.

Mendaftarkan aset ke registry Rome memperlakukan verifikasi sebagai **gerbang**, bukan dekorasi (`register-asset.ts`) : token memperoleh `apps/arc/<chain>.json` catatan — membawa `standard: arc-permissioned-erc20` dan `transferRestriction` yang menamai gerbang allowlist-nya — hanya ketika ketiga kontraknya terverifikasi di Sourcify. Catatan itu membaca chain untuk faktanya (token yang dideploy lewat wizard tidak memiliki receipt yang dikomit) dan hanya menulis checkout registry lokal; mengajukan PR adalah tugas manusia.

```bash
CHAIN_ID=200010 npx tsx register-asset.ts <tokenAddress>        # verifikasi + siapkan catatan registry
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # pratinjau read-only
```

Untuk bukti end-to-end dari seluruh loop — buat, whitelist, gate, jual di kedua jalur, distribusikan yield — jalankan smoke yang didanai pada chain mana pun:

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

***

## Langkah 10 — Ketahui apa yang tetap off-chain

Model kepercayaan ini sengaja jujur, dan builder sebaiknya menyajikannya dengan cara yang sama kepada penggunanya sendiri:

* **Flag allowlist adalah keputusan off-chain Anda.** KYC/AML terjadi dalam proses Anda — vendor Anda, aturan Anda. Chain hanya mencatat dan menegakkan *hasil*, `isWhitelisted(addr)`.
* **Satu-satunya state on-chain adalah satu boolean per alamat.** Tidak ada PII, tidak ada dokumen, tidak ada klaim identitas, tidak ada attestation on-chain, tidak ada bukti kriptografis tentang siapa pemilik sebuah alamat. Ini adalah model Arc yang digunakan di Plume; model ini menukar transfer yang lebih berat dari standar identitas on-chain (mis. ERC-3643) dengan mekanisme yang lebih ringan dan kepercayaan yang dipegang issuer.
* **Pemulihan adalah kekuasaan issuer, bukan jalan keluar darurat.** Wallet yang hilang — EVM atau Solana — dipulihkan dengan me-whitelist alamat pengganti dan, jika perlu, menggunakan mint/burn/upgrade di bawah proses hukum Anda sendiri (Arc tidak memiliki primitive forced-transfer bawaan). Karena itu, kustodi kunci issuer adalah bagian dari posture kepatuhan.
* **Tidak ada relayer, tidak ada permukaan kustodi.** Pengguna memegang gas mereka sendiri di kedua jalur. Layanan fee-payer berarti memegang kunci — permukaan yang seharusnya tidak ditambahkan oleh produk RWA.

***

## Referensi: tooling

Setiap skrip dijalankan dari `scripts/` setelah `npm install`, dengan `PRIVATE_KEY`, `CHAIN_ID`, dan `REGISTRY_ROOT` diekspor.

| Skrip                           | Apa fungsinya                                                                 |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `deploy-infra.ts`               | Sekali per chain: empat kontrak bersama, dikonfigurasi untuk wUSDC.           |
| `create-token.ts`               | Wizard bertahap — delapan transaksi, satu token.                              |
| `rotate-factory.ts`             | Deploy factory baru setelah perubahan compiler/artifact; sambungkan ulang.    |
| `verify-artifact-provenance.ts` | Buktikan artifact proxy Anda diterima oleh factory yang dideploy (read-only). |
| `verify-deployments.ts`         | Verifikasi ketiga kontrak per token + infrastruktur di Sourcify (read-only).  |
| `register-asset.ts`             | Verifikasi gerbang, lalu siapkan catatan kepatuhan registry.                  |
| `measure-yield-batch.ts`        | Ukur ulang batas yield per tx pada sebuah chain.                              |
| `smoke.ts`                      | Suite end-to-end yang didanai di kedua jalur.                                 |
| `sweep-synthetic-native.ts`     | Ambil kembali gas native dari akun sintetis (kebersihan pengujian).           |

Latar belakang yang lebih mendalam tersedia bersama panduan ini: `docs/ARCHITECTURE.md` (model aplikasi berlapis), `docs/LANES.md` (kedua jalur dan kustodi), `docs/COMPLIANCE.md` (model kepercayaan), dan `docs/USAGE.md` (runbook 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 dynamically by asking a question.

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

```
GET https://docs.rome.builders/id/aplikasi-di-rome/bloom/building.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
