> 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/zh/rome-shang-de-ying-yong/bloom/building.md).

# 在 Bloom 上构建

本指南回答一个问题： **构建者如何自行完成这件事？** 将 Solidity 现实世界资产合约部署到 Rome 链上作为许可型代币，无需桥接、无需第二种代币，即可向 Solana 钱包和 EVM 钱包开放，并接入你自己的 KYC/合规决策。

目标读者是正在评估或采用此方法的开发者。Bloom 是贯穿全文的实例——其在线资产 **ARCV（“矿产金库 I”）** 位于 Hadrian（`200010`）上，是下文每一步的真实实例；其部署回执（`deployments/200010.tokens/ARCV.json`）记录了此方法产生的确切三个合约和八笔交易。

这里没有修改资产合约。Bloom 运行的是 Plume 的 Arc 框架， **未作修改** （位于 `contracts/`中的供应商引入代码树，与 `plumenetwork/contracts` 在 `NOTICE`中指定版本的字节码完全相同）；Rome 侧唯一的合约是 `ArcTokenFactoryV2`，它只增加了一个函数。工作重点在于 *如何* 部署并 *如何* 驱动这些合约——而不是修改它们。

> **方法概览。** 一个许可型代币由 **三个** 已部署合约组成。它以 **分步方式** 部署——每个合约一笔交易——因为单笔交易的工厂调用会超过 Rome 的单笔交易账户上限。其代理必须来自 **来源锁定的构件** ，否则链上注册会安全失败。合规性是每个地址一个 **布尔值**，由你的 KYC 流程授权的人员写入。同一部署还可通过链上派生的 **合成发送方** 从 Solana 钱包访问——无需桥接、无需封装代币、无需第二份允许列表。

## 前提条件

所有参考工具都位于 `scripts/` （先在那里运行 `npm install` ）。每条命令均从注册表解析链信息，而非将其硬编码，因此改用另一条 Rome 链只需改变环境，无需修改代码：

```bash
export PRIVATE_KEY=…       # 目标链上有资金的部署者/发行方密钥
export CHAIN_ID=200010     # 选择该链
export REGISTRY_ROOT=…     # Rome 链注册表的一个检出副本
# Solana 通道流程还需要：
export SOLANA_KEYPAIR=…    # 有资金的 Solana 密钥对 JSON 路径（费用支付者）
```

Rome 链上的 Gas 以 USDC 计价；在开发网上，完整部署序列的成本约为 10–15 个原生单位。密钥仅来自环境变量，绝不来自仓库。部署前先构建一次合约，因为构件 **必须** 来自 `rome-contracts/out` （见步骤 3）：

```bash
cd contracts       && forge build --via-ir     # 引入的 Arc 套件
cd rome-contracts  && forge build --via-ir     # ArcTokenFactoryV2 + 规范代理
```

***

## 步骤 1——了解这里的许可型代币 *是* 什么

一个许可型 Bloom 代币并非单一合约。它是 **三个已部署合约**，外加四个每条链由运维部署一次、每个发行方都会复用的共享合约。

**每个代币对应的三个合约** ——每项资产恰好拥有以下合约：

| 合约                           | 来源                                                          | 说明                                                 |
| ---------------------------- | ----------------------------------------------------------- | -------------------------------------------------- |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | 代币本身——一个指向共享 `ArcToken` 实现的 UUPS 代理。余额、持有人和角色均在此处。 |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | 允许列表模块。每个地址一个布尔值；资产受限时，每笔转账均会执行该模块。                |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | 收益模块。在不影响持仓的情况下，将地址排除在收益分配之外。                      |

**四个共享的、每条链一个的合约** （由 `deploy-infra.ts`):

| 合约                   | 角色                      |
| -------------------- | ----------------------- |
| `RestrictionsRouter` | 模块类型注册表——链路接线，由运维设置一次。  |
| `ArcTokenFactoryV2`  | 注册以及规范代理代码哈希（步骤 2–3）。   |
| `ArcToken` （实现）      | 每种代币代理所指向的共享、可复用资产逻辑。   |
| `ArcTokenPurchase`   | 一个由每种代币和发行方共享的店面（步骤 4）。 |
| wUSDC                | 资金端——收益货币和销售购买货币。       |

转账限制和收益限制是 **刻意分离的模块**：持有人可以在保留资产的同时被禁止获得收益，这正是制裁和法院命令在现实中的运作方式。

这个“三个合约”的事实是验证的关键基础。 `verify-deployments.ts` 和 `register-asset.ts` 都要求一个代币能解析为三个已部署合约——曾有一次手动验证代币时，遗漏了一条链上的七个合约（每个收益模块和三个允许列表模块）。

生命周期有三个阶段和一道单向门：

```
草稿     代理已上线，未链接模块——没有任何机制限制转账
  ↓
未受限   模块已链接，transfersAllowed = true   （WhitelistRestrictions.initialize 会设置此值）
  ↓  铸造供应量，建立允许列表               [可逆]
受限     setTransfersAllowed(false)                [单向门——供应量成为最终值]
```

供应量的限制是单向的，因为铸造和销毁会传入 `address(0)` 作为交易对手方，并且该地址 `address(0)` 永远无法加入允许列表——因此一旦受限，就再也不能铸造新的供应量。这就是许可型资产向其持有人作出的保证。

***

## 步骤 2——分步部署，绝不要作为单笔交易部署

Arc 提供一个一次性调用 `createToken` ，可在单次调用中部署代理和两个模块，并完成接线。 **在 Rome 上不能使用它。** 该交易需要 **77 个 Solana 账户锁**，而 Rome 的单笔交易上限是 **62 个锁**。它永远无法容纳——无论调优还是进行优化器处理都不行。

因此，部署采用 **分步方式**：每个合约各自一笔交易，并且代币通过 `ArcTokenFactoryV2.registerToken` 而非单体式 `createToken`获得等同工厂创建的状态。这是创建向导，也是发行方控制台逐步执行的流程：

![创建向导](/files/d9a456533bc682871cdd7ec4473de80b8674baac)

参考实现是 `arc/plan/create.ts` (`createSequence()`），应用将其呈现为六张卡片（`bloom/lib/createCards.ts`），共计 **八笔交易**，顺序严格由合约决定：

| # | 交易       | 合约调用                                                                         |
| - | -------- | ---------------------------------------------------------------------------- |
| 1 | 部署资产     | `new ArcTokenProxy(impl, initData)` 其中 `initData = ArcToken.initialize(...)` |
| 2 | 部署允许列表模块 | `new WhitelistRestrictions()`                                                |
| 3 | 将你设为其管理员 | `whitelist.initialize(issuer)`                                               |
| 4 | 在转账时强制执行 | `token.setRestrictionModule(TRANSFER, whitelist)`                            |
| 5 | 部署收益模块   | `new YieldBlacklistRestrictions()`                                           |
| 6 | 将你设为其管理员 | `yieldBlacklist.initialize(issuer)`                                          |
| 7 | 在收益时强制执行 | `token.setRestrictionModule(YIELD, yieldBlacklist)`                          |
| 8 | 向工厂注册    | `factoryV2.registerToken(token, impl)`                                       |

脚本形式只需一条命令：

```bash
# 每条链一次（运维操作，不是每个代币一次）——部署四个共享合约
npx tsx deploy-infra.ts

# 每个代币一次——通过脚本重放创建向导
NAME="Mineral Vault I" SYMBOL=ARCV SUPPLY=1000000 DECIMALS=6 npx tsx create-token.ts
```

发行方（`PRIVATE_KEY`）签署每笔交易，并最终持有代币的角色（`ArcToken.initialize` 将角色授予 `msg.sender` ——见步骤 7）。注册（交易 8）会解锁店面的 `enableToken` 以及任何由工厂介导的升级。

> **不要将其“优化”回单体式方案。** 将这些调用批量合并为一笔交易以节省往返次数，会重新引入分步路径旨在避免的 62 锁溢出。同一上限也是收益分配每笔交易仅处理一个持有人的原因（步骤 8）。如果调用失败并显示 `Too many accounts: N > 62`，则表示一笔交易触及了过多账户——应拆分它，而非调优。

***

## 步骤 3——仅从来源锁定的构件部署代理

`registerToken` 是确保分步部署安全的安全边界。它只接受其 **运行时代码哈希与规范代码哈希相匹配的代币，该规范代码哈希在工厂部署时 `ArcTokenProxy`** 被固化进工厂：

```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();
    // ……实现必须在工厂白名单中，且 msg.sender 必须持有
    //    代币的 ADMIN_ROLE，且该代币尚未注册。
}
```

由于该代码哈希是 **不可变的**，由任何其他构件构建的代理都会以 `ProxyCodehashUnknown`安全失败。由此产生两条规则，二者均由 CI 和 `CLAUDE.md`:

1. **中的强制规则执行：代币代理必须从 `rome-contracts/out`** 部署——绝不能从 `contracts/out`部署，也绝不能从不同的构建产物部署。这正是工厂的规范代码哈希所固定的编译结果。
2. **`bytecode_hash = "none"` 和 `cbor_metadata = false`** 在 `rome-contracts/foundry.toml` 中的设置至关重要。启用元数据后， `ArcTokenProxy` 运行时 *被嵌入* 在工厂中的`type().runtimeCode`会携带与独立 `out/` 构件不同的 CBOR/IPFS 尾部，创建向导从后者部署——因此链上 `registerToken` 会回退，而 **所有源代码内的 Foundry 测试都会通过** （Foundry 会嵌入两个副本，因此无法发现这一不匹配）。 `rome-contracts/test/ArtifactProvenance.t.sol` 固定了这一属性；最初发现它的是一次有资金的运行。

> **源代码内测试套件全绿并不能证明来源。** 在发布镜像之前，以及任何编译器设置变更之后，请针对 *已部署的* 工厂验证此属性（只读，无需密钥）：
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> 它会对你的本地 `ArcTokenProxy` 构件进行哈希，并断言该哈希原样出现在已部署工厂的字节码中。

如果必须变更编译器设置， **轮换工厂** ——部署一个新的工厂，其不可变值固定新的代码哈希，然后重新接线——而不是手动修补检查：

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

轮换会在回执中保留旧地址；针对旧工厂注册的代币必须重新向新工厂注册。

***

## 步骤 4——通过共享店面开售

销售是独立于创建的行为，且在唯一共享的 `ArcTokenPurchase` 店面上进行。销售开启前必须满足三项条件，且 `ArcTokenPurchase.enableToken` 会在链上检查每一项：

```solidity
// contracts/src/ArcTokenPurchase.sol
function enableToken(address _tokenContract, uint256 _numberOfTokens, uint256 _tokenPrice)
    external onlyTokenAdmin(_tokenContract)                       // (a) 你持有该代币的 ADMIN_ROLE
{
    // (b) 该代币必须为工厂已知——这正是 registerToken（步骤 2）带来的结果：
    if (ArcTokenFactory(ps.tokenFactory).getTokenImplementation(_tokenContract) == address(0))
        revert TokenNotCreatedByFactory();
    // (c) 店面必须已持有销售库存：
    if (ArcToken(_tokenContract).balanceOf(address(this)) < _numberOfTokens)
        revert ContractMissingRequiredTokens();
    // ……价格和数量必须为正数。
}
```

受限资产还强制要求第四个隐含条件：由于 **每笔转账都会经过允许列表门控**，只有当店面地址本身已被加入你的代币允许列表时，才能成功将库存转移至店面。因此，完整的开售序列为：

1. **将店面加入白名单** 在你的代币允许列表模块中（`batchAddToWhitelist([storefront])` ——见步骤 5）。若未完成此操作，步骤 (2) 中的库存转移在受限后会回退 `TransferRestricted()` 。
2. **转移库存** 至店面地址（来自基础设施回执中的 `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** ——买方现在支付 wUSDC 并获得 RWA。

在 *共享* 店面上，了解提款权限划分很重要，且它并不对称（`arc/roles.ts` 是权威来源）：

| 操作                                                         | 权限                                  | 持有人     |
| ---------------------------------------------------------- | ----------------------------------- | ------- |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | 代币的 `ADMIN_ROLE` (`onlyTokenAdmin`) | 你，即发行方  |
| `withdrawPurchaseTokens` （wUSDC 收益）                        | 店面的 `DEFAULT_ADMIN_ROLE`            | 部署店面的人员 |

在自托管链上，你同时拥有两种权限。在共享店面上，收益池汇集了每个发行方的收入，因此其提现属于平台级操作——应据此设计结算流程，而不是假定自己拥有单方面控制权。

***

## 步骤 5——接入你自己的 KYC / 合规体系

这是客户在表示同意前会阅读的章节，因此它是代码，而非文字说明。 **合规性由代币合约本身强制执行** ——而非链下筛查、排序器过滤或场所政策。这些规则随代币进入任何执行路径，覆盖两种钱包通道。链上记录的是你 KYC 决策的 **结果** ，即一个布尔值：

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol ——完整的链上状态
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← 每个地址一个布尔值。绝不存储个人身份信息。
    bool transfersAllowed;                    // false = 受限；只有白名单地址可以转账
    EnumerableSet.AddressSet whitelistedAddresses;
}
```

### 你的流程写入的接口

你的 KYC 供应商的批准/拒绝决策恰好会转化为以下调用之一：

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

### 谁可以写入

> **批准所需的权限是 `MANAGER_ROLE` 位于&#x20;*****模块*****&#x20;上——而不在 `ADMIN_ROLE` 代币上。** `addToWhitelist` / `batchAddToWhitelist` / `removeFromWhitelist` 均为 `onlyRole(MANAGER_ROLE)`，且该角色存在于 `WhitelistRestrictions` 实例上。身份验证 `ADMIN_ROLE` 先在代币上操作，然后再写入模块，是这个代码库已经为之付出代价的真实缺陷。不要提供 `WHITELIST_ADMIN_ROLE` 作为拒绝的修复方案——它在 initialize 时授予，并且 **在整个测试套件中从未检查** (`arc/roles.ts` 断言它是那个不限制任何内容的唯一角色）。

`WhitelistRestrictions.initialize(issuer)` （向导的第 3 笔交易）向发行方授予 `DEFAULT_ADMIN_ROLE`, `ADMIN_ROLE`, `MANAGER_ROLE`, `WHITELIST_ADMIN_ROLE`，并且 `UPGRADER_ROLE` 在该模块上。因此，发行方持有 `MANAGER_ROLE` 开箱即用，并且还可以委派它：因为发行方持有 `DEFAULT_ADMIN_ROLE`，一个 `grantRole(MANAGER_ROLE, serviceAccount)` 会将写入权限交给后端签名者。

### 一个决策变成链上写入

将你的供应商 webhook 指向一个持有以下权限的签名者： `MANAGER_ROLE`，并在批准时批量处理这些地址：

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

// `signer` 必须在该代币的白名单模块上持有 MANAGER_ROLE。
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// 供应商对这些地址表示“已批准”：
await wallet.writeContract({
  address: whitelistModule,                 // 通过 getRestrictionModule 从代币中解析
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

解析 `whitelistModule` 从代币本身（`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`），而不是从缓存的投影中获取——将写入指向错误的模块，正是“对任意资产进行批准”曾经写入某个代币白名单的原因。

### 钱包签名版本（自助演示）

对于一个访客可自行进入的无需许可测试部署，授予 `MANAGER_ROLE` 给一个 `TestApprover` (`rome-contracts/src/TestApprover.sol`），其 `模块` 是 **不可变的**。然后 `approve(address)` 可由任何人使用自己的钱包、支付自己的 gas 来调用——它会调用 `addToWhitelist` 仅当该地址尚未列出时：

```solidity
function approve(address account) external {
    IWhitelistRestrictions target = IWhitelistRestrictions(module);   // 不可变——一个模块，永久如此
    if (!target.isWhitelisted(account)) target.addToWhitelist(account);
    emit Approved(account, msg.sender);
}
```

这就是“登录门禁”——它向访客展示真实机制（批准变成链上的允许名单条目，随后由代币执行），而不托管任何资产。 **切勿授予 `MANAGER_ROLE` 给一个 `TestApprover` 在生产代币上** ——这会让该代币的允许名单变成无需许可。

### 当门禁生效，以及切换供应商

只有在资产被 **加上门禁后，** (`setTransfersAllowed(false)`, `ADMIN_ROLE` 在模块上）。在未加门禁时，转账是开放的——先构建你的允许名单，再启用门禁。一旦加上门禁，任一方不在名单中的转账都会以类型化错误回滚 `TransferRestricted()` (`0xe827105e`).

> **之后切换 KYC 提供方不需要任何链上变更。** 允许名单模块、其接口以及它存储的布尔值都与提供方无关。要切换供应商，只需将另一个决策来源指向同一个 `MANAGER_ROLE` 签名者——无需重新部署、无需迁移、无需状态变更。链上从不知道是哪个供应商发起了调用，只知道结果。

***

## 第 6 步——让同一资产面向 Solana 钱包开放

同样的三个合约可从 Solana 钱包访问 **无需桥接，也无需第二个代币**。原生 Solana 用户在任何地方都没有 EVM 密钥；取而代之的是，他们的 EVM 身份是 *合成的*，由交易实际的 Solana 签名者在链上派生：

```
合成的 EVM 地址 = keccak256(solana_pubkey)[12..32]
```

Rome EVM 程序直接从签名者本身派生此地址（`do_tx_unsigned::derive_sender`），因此无法伪造，而且不存在对应的 secp256k1 私钥—— **Solana 签名是唯一能够驱动该地址的东西**。参考派生逻辑位于 `arc/materialise/solana/identity.ts` (`syntheticAddress`），与链上规则字节兼容：

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

**构建者为支持该通道所做的事情** ——请注意 *其中没有任何一项属于合约更改*:

1. **根据合成地址将该 Solana 用户加入白名单。** 对合规层而言，合成地址就是普通地址：粘贴用户的 Solana 公钥，派生出合成地址，然后 `batchAddToWhitelist([synthetic])`。一个允许名单同时覆盖两种钱包世界。
2. **在其第一次交易之前，先为其配置一次签名 PDA。** 用于授权某个合成地址签名的外部授权 PDA 必须先存在，而创建它是一个 EVM 通道调用（`create_pda`）——纯 Solana 用户无法自行完成，因此由发行方进行配置。配置是 `external_auth`，而不是懒加载。
3. **通过通道库提交，而不是手工提交。** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`）会构建一个 `DoTxUnsigned` 指令——一个 *未签名的* 由 Solana 签名授权的 EIP-1559 负载——并处理手工构建交易最容易出错的四件事：
   * **账户发现** 通过 `rome_emulateCallAccounts`，它决定每个账户的可写性。调用的 `value` **必须** 必须转发给发现逻辑，否则一次 value 转账会将接收方的余额 PDA 标记为只读并在链上失败。
   * **计算预算** ——一个 `DoTxUnsigned` 需要一个 **250 KB 堆帧** 以及一个 **135 万 CU** 限额（Rome 的 EVM 会超出默认的 32 KB / 20 万上限）；为 Solana 的 140 万上限预留约 5 万余量。
   * **宝库（手续费）钱包 PDA**，作为可写附加项——发现逻辑会忽略它。
   * **v0 + 查找表回退方案。** 当某次调用的账户数量超过 1232 字节的旧版交易封装时，客户端会通过新的地址查找表重新以 v0 交易提交。（按今天的测量，店面购买在旧版封装中约占 1,061/1232 字节；而首次持有者若其关联代币账户尚待创建，则可能超过该上限。）

用户是 **手续费支付者** ，并从其 Solana 钱包支付 lamports（SOL）；他们控制的合成地址持有资产，但从不持有 SOL。每次通道发送后，在下一次之前轮询 EVM nonce——索引器会略微落后于 Solana 确认。

> **该通道仅支持原子执行。** 一个 `DoTxUnsigned` 是由 Rome 的原子 VM 在一笔 Solana 交易内执行的一笔 EVM 交易。迭代 VM（一次 EVM 执行分布在多笔 Solana 交易中） **目前尚无法通过该通道到达**。无法以原子方式容纳的调用根本不能走这条通道——修复方式是让调用更小，而不是回退。请注意，v0+ALT 回退是一种更大的 *封装*，而不是不同的执行；它仍然是一笔原子交易。

**为什么 RWA 不会泄漏出边界。** 该资产驻留于 EVM 状态中——其余额是 Rome EVM 程序账户内的存储。不存在 **SPL 铸币** ，也没有任何原生 Solana 侧可移动的对象；该通道的资产移动原语作用于 SPL 代币账户，而 EVM 存储余额并不具备这类账户。Solana 签名能够表达的每一次移动，都会进入代币合约调用，并执行允许名单门禁——已资助的测试套件断言，向非白名单地址转账的回滚结果在 EVM 密钥或 Solana 密钥签名下完全一致。现金部分（wUSDC）是有意的例外：它由 SPL 支持，并且 *会* 转出到用户自己的代币账户。

***

## 第 7 步——了解部署后谁持有权限

`ArcToken.initialize` 由代理构造函数（第 1 笔交易）执行，并将角色授予 `msg.sender` ——即向导驱动者，也就是发行方。它明确授予 **五个** 角色：

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

构建者绝不能忽视的两个后果：

* **`MINTER_ROLE`, `BURNER_ROLE`，并且 `UPGRADER_ROLE` 在 initialize 时不会被授予。** 初始供应量是在 `initialize` （一个内部 `_mint`，不需要角色），但 *之后的* `mint` 或 `burn` 则要求发行方先给自己授予该角色。他们可以这么做，因为他们持有 `DEFAULT_ADMIN_ROLE` （每个角色的管理员——没有任何地方调用 `_setRoleAdmin`，因此 `DEFAULT_ADMIN_ROLE` 管理着所有角色）。但这是一个刻意的额外步骤，而不是默认可用的能力。
* **`UPGRADER_ROLE` 从不授予工厂。** 通过工厂中介的升级（`ArcTokenFactoryV2.upgradeToken`）要求 **工厂本身** 持有 `UPGRADER_ROLE` 在代币上的该权限——授予给自己不会产生任何作用。该授予是一扇单向门，工厂无法撤销，因此是可选启用的：如果你想要工厂中介升级， `grantRole(UPGRADER_ROLE, factory)` 就显式调用；否则由发行方直接升级代币。

完整的 21 权限模型（六个合约中的 11 个角色名称——同名在不同合约上代表不同权限）已编写在 `arc/roles.ts`。请查询 `rolesRequiredFor(contract, functionName)` 而不是读取角色自身所在的合约，因为四个前台函数和两个工厂函数都受制于位于另一个 *不同的* 合约上的权限。

***

## 第 8 步——分发收益

代币以 wUSDC 支付收益（固定为 `initialize`，可通过 `setYieldToken` 在 `YIELD_MANAGER_ROLE`下可更改）。分发会遍历持有人集合：

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

正确驱动它时需要注意的机制：

* **完整的 `totalAmount` 仅在 `startIndex == 0` 窗口** (`safeTransferFrom(msg.sender, this, totalAmount)`）。在第一个窗口之前先向代币批准该总额；后续窗口则从已持有的余额中支付。
* **`nextIndex` 在……时回绕到 `0` 当遍历完成时。** 循环调用 `distributeYieldWithLimit(total, nextIndex, …)` 直到 `nextIndex` 回到 0 `0`.
* **受收益限制的持有者份额保留在代币合约中** ——它们会被跳过，而不是重新分配。

> **在 Rome 上，实际上的上限是 `maxHolders = 1` 每笔交易** ——与第 2 步相同的 62 账户上限——因此每笔交易只遍历一个持有人（实测每个持有人约 318K gas）。每个白名单持有人，无论是 EVM 还是原生 Solana 用户，都会按比例获得收益；参考脚本是 `measure-yield-batch.ts` ，而资助版 smoke 测试中的收益步骤会对其进行端到端断言。

```bash
# USAGE.md 参考形式：每笔交易遍历一个持有人，直到 nextIndex 回绕到 0
distributeYieldWithLimit(total, offset, 1)
```

***

## 第 9 步——验证你部署了什么

Rome 不会自动将源码提交到任何地方，因此验证是你需要手动执行的一步。每次部署后——无论是基础设施还是新代币——都运行只读、无密钥、幂等的验证器（已验证过的合约会被跳过，因此在发行代币后再次运行只会验证新增的合约）：

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

它期望 **三个** 每个代币对应的合约（代理合约 + 两个模块）以及基础设施，并报告哪些无法验证，而不是让部署失败。手动做过一次后曾漏掉七个合约。

将资产注册到 Rome 注册表时，会把验证视为一个 **门禁**，而不是装饰（`register-asset.ts`）：代币只有在 `apps/arc/<chain>.json` 记录——携带着 `standard: arc-permissioned-erc20` 以及一个 `transferRestriction` 为其允许名单门禁命名——只有当其三个合约都在 Sourcify 上验证通过时才行。该记录从链上读取事实（向导部署的代币没有已提交的收据），并且只写入本地注册表检出；提交 PR 仍然是人的工作。

```bash
CHAIN_ID=200010 npx tsx register-asset.ts <tokenAddress>        # 验证 + 准备注册表记录
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # 只读预览
```

若要对整个闭环进行端到端证明——创建、加入白名单、启用门禁、在两条通道上出售、分发收益——请在任意链上运行资助版 smoke 测试：

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

***

## 第 10 步——了解哪些内容留在链下

信任模型是刻意保持透明的，构建者也应当以同样方式向自己的用户呈现：

* **允许名单标志是你的链下决策。** KYC/AML 发生在你的流程中——你的供应商，你的规则。链上仅记录并执行 *结果*, `isWhitelisted(addr)`.
* **链上唯一的状态是每个地址一个布尔值。** 没有 PII，没有文档，没有身份声明，没有链上证明，也没有关于地址归属对象的密码学证明。这是 Arc 在 Plume 上使用的模型；它用更轻量的机制和发行方持有的信任，换取了链上身份标准（例如 ERC-3643）中更沉重的转移成本。
* **恢复是发行方的权限，而不是逃生通道。** 无论是 EVM 还是 Solana，丢失的钱包都可以通过将替代地址加入白名单来恢复，必要时再在你自己的法律流程下使用 mint/burn/upgrade（Arc 没有内置强制转账原语）。因此，发行方的密钥托管也是合规姿态的一部分。
* **没有中继，也没有托管面。** 用户在两条通道上都自行持有 gas。手续费支付服务则意味着要持有密钥——这不是 RWA 产品应额外引入的攻击面。

***

## 参考：工具

每个脚本都从 `scripts/` 之后 `npm install`，并且 `PRIVATE_KEY`, `CHAIN_ID`，并且 `REGISTRY_ROOT` 被导出。

| 脚本                              | 功能                                    |
| ------------------------------- | ------------------------------------- |
| `deploy-infra.ts`               | 每条链一次：四个共享合约，已连接到 wUSDC。              |
| `create-token.ts`               | 分步向导——八笔交易，一个代币。                      |
| `rotate-factory.ts`             | 在编译器/工件变更后部署一个新的工厂；重新连接。              |
| `verify-artifact-provenance.ts` | 证明你的代理工件被已部署的工厂接受（只读）。                |
| `verify-deployments.ts`         | 在 Sourcify 上验证每个代币的全部三个合约 + 基础设施（只读）。 |
| `register-asset.ts`             | 先验证门禁，然后准备注册表合规记录。                    |
| `measure-yield-batch.ts`        | 重新测量某条链上的每笔交易收益上限。                    |
| `smoke.ts`                      | 覆盖两条通道的资助版端到端测试套件。                    |
| `sweep-synthetic-native.ts`     | 从合成账户中回收原生 gas（测试清理）。                 |

更深入的背景资料与本指南并列提供： `docs/ARCHITECTURE.md` （分层应用模型）， `docs/LANES.md` （两条通道与托管）， `docs/COMPLIANCE.md` （信任模型），以及 `docs/USAGE.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 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/zh/rome-shang-de-ying-yong/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.
