> 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/ru/prilozheniya-na-rome/bloom/building.md).

# Создание в Bloom

Это руководство отвечает на один вопрос: **как разработчик делает это самостоятельно?** Возьмите контракт Solidity для реального актива, разверните его в цепочке Rome как разрешённый токен, откройте его как для кошельков Solana, так и для EVM-кошельков без моста и без второго токена, и подключите собственное решение KYC/комплаенса.

Аудитория — это разработчик, который оценивает или внедряет данный подход. Bloom на протяжении всего текста служит практическим примером — его актив **ARCV ("Mineral Vault I")** на Hadrian (`200010`) — это реальный экземпляр каждого шага ниже, а его квитанция о развертывании (`deployments/200010.tokens/ARCV.json`) фиксирует ровно три контракта и восемь транзакций, которые создаёт этот метод.

Ничто здесь не изменяет контракты актива. Bloom использует фреймворк Arc от Plume **без изменений** (вендоризированное дерево в `contracts/`, побайтно идентичное `plumenetwork/contracts` на закреплённой версии в `NOTICE`); единственный контракт на стороне Rome — это `ArcTokenFactoryV2`, который добавляет одну функцию. Работа заключается в *том, как* вы разворачиваете и *том, как* управляете контрактами — а не в их изменении.

> **Метод в общих чертах.** Разрешённый токен — это **три** развернутых контракта. Он разворачивается **поэтапно** — по одному контракту на транзакцию — потому что вызов фабрики в одной транзакции превышает лимит Rome на количество аккаунтов в транзакции. Его прокси должны происходить из **артефактов с закреплённым происхождением** иначе ончейн-регистрация завершится неудачей по принципу fail-closed. Комплаенс — это один **булев флаг на адрес**, записываемый тем, кого авторизует ваш KYC-процесс. А то же самое развертывание доступно из кошелька Solana через **синтетический отправитель** который выводится ончейн — без моста, без обёрнутого токена, без второго allowlist.

## Требования

Весь эталонный инструментарий находится в `scripts/` (сначала выполните `npm install` там). Каждая команда получает сведения о цепочке из реестра, а не задаёт их жёстко, поэтому перенос на другую цепочку Rome — это изменение окружения, а не кода:

```bash
export PRIVATE_KEY=…       # ключ финансируемого деплойера/эмитента в целевой цепочке
export CHAIN_ID=200010     # выбирает цепочку
export REGISTRY_ROOT=…     # checkout реестра цепочек Rome
# Для потоков по линии Solana также требуется:
export SOLANA_KEYPAIR=…    # путь к JSON с финансируемой парой ключей Solana (плательщик комиссий)
```

Газ в цепочках Rome номинирован в USDC; полная последовательность развертывания стоит порядка 10–15 нативных единиц в devnet. Ключи берутся только из окружения — никогда из репозитория. Соберите контракты один раз перед развертыванием, потому что артефакты **должны** поступать из `rome-contracts/out` (см. шаг 3):

```bash
cd contracts       && forge build --via-ir     # вендоризированный набор Arc
cd rome-contracts  && forge build --via-ir     # ArcTokenFactoryV2 + канонический прокси
```

***

## Шаг 1 — Поймите, что такое разрешённый токен *есть* здесь

Разрешённый токен Bloom — это не один контракт. Это **три развернутых контракта**, плюс четыре общих контракта, которые ops разворачивает один раз на цепочку, а каждый эмитент использует повторно.

**Три контракта на каждый токен** — у одного актива именно эти:

| Контракт                     | Источник                                                    | Что это такое                                                                                                       |
| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | Сам токен — UUPS-прокси, указывающий на общий `ArcToken` реализацию. Балансы, держатели и роли находятся здесь.     |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | Модуль allowlist. Один булев флаг на адрес; применяется при каждом переводе, пока актив находится под ограничением. |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | Модуль доходности. Исключает адреса из распределения доходности, не затрагивая их владение.                         |

**Четыре общих контракта, по одному на цепочку** (развернуты `deploy-infra.ts`):

| Контракт                | Роль                                                                              |
| ----------------------- | --------------------------------------------------------------------------------- |
| `RestrictionsRouter`    | Реестр типов модулей — цепочная настройка, выполняется ops один раз.              |
| `ArcTokenFactoryV2`     | Регистрация + канонический codehash прокси (шаги 2–3).                            |
| `ArcToken` (реализация) | Общая, многократно используемая логика актива, к которой проксирует каждый токен. |
| `ArcTokenPurchase`      | Одна витрина, общая для каждого токена и эмитента (шаг 4).                        |
| wUSDC                   | Денежная сторона — валюта доходности и валюта продажи/покупки.                    |

Ограничение переводов и ограничение доходности **преднамеренно являются отдельными модулями**: держателю может быть запрещён доход при сохранении актива, и именно так на практике работают санкции и судебные распоряжения.

Этот факт про «три контракта» критически важен для верификации. `verify-deployments.ts` и `register-asset.ts` оба ожидают, что токен будет разрешаться к трём развернутым контрактам — ручная проверка одного токена однажды пропустила семь из них в одной цепочке (все модули доходности и три модуля whitelist).

Жизненный цикл состоит из трёх фаз и одной двери только в одну сторону:

```
draft     прокси жив, модуль не привязан — ни один перевод не ограничен
  ↓
ungated   модуль привязан, transfersAllowed = true   (WhitelistRestrictions.initialize задаёт это)
  ↓  выпуск объёма, формирование allowlist               [обратимо]
gated     setTransfersAllowed(false)                [ДВЕРЬ ТОЛЬКО В ОДНУ СТОРОНУ — объём становится окончательным]
```

Ограничение доступно только в одну сторону для объёма, поскольку функции mint и burn используют `address(0)` в качестве контрагента, и `address(0)` никогда не может быть включён в allowlist — поэтому после введения ограничений новый объём уже невозможно создать. Именно такую гарантию разрешённый актив даёт своим держателям.

***

## Шаг 2 — Разворачивайте поэтапно, никогда не одной транзакцией

Arc поставляет одношаговый `createToken` который разворачивает прокси и оба модуля и связывает их одним вызовом. **В Rome использовать его нельзя.** Для этой транзакции требуется **77 блокировок аккаунтов Solana**, а лимит Rome на одну транзакцию составляет **62 блокировки**. Оно никогда не поместится — ни при какой настройке, ни при каком проходе оптимизации.

Поэтому развертывание **поэтапно**: каждый контракт в отдельной транзакции, а токен получает статус, эквивалентный фабрике, через `ArcTokenFactoryV2.registerToken` вместо монолитного `createToken`. Это мастер создания, и именно так консоль эмитента выполняет процесс шаг за шагом:

![мастер создания](/files/f7f16c15a56803ca01d085f62ed2f3a8eae3344f)

Эталонная реализация — это `arc/plan/create.ts` (`createSequence()`), который приложение отображает как шесть карточек (`bloom/lib/createCards.ts`) поверх **восьми транзакций**, в точном порядке, который задают контракты:

| # | Транзакция                        | Вызов контракта                                                               |
| - | --------------------------------- | ----------------------------------------------------------------------------- |
| 1 | Развернуть актив                  | `new ArcTokenProxy(impl, initData)` где `initData = ArcToken.initialize(...)` |
| 2 | Развернуть модуль allowlist       | `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
# один раз на цепочку (ops, не на каждый токен) — развертывает четыре общих контракта
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). Регистрация (tx 8) — это то, что открывает витрине `enableToken` и любое обновление, выполняемое через фабрику.

> **Не «оптимизируйте» это обратно в монолит.** Пакетирование этих вызовов в одну транзакцию ради сокращения числа обращений возвращает ровно тот overflow на 62 блокировки, которого поэтапный путь и призван избежать. Тот же лимит объясняет, почему распределение доходности проходит по одному держателю на транзакцию (шаг 8). Если вызов завершается ошибкой `Too many accounts: N > 62`, одна транзакция затрагивает слишком много аккаунтов — разделите её, не настраивайте.

***

## Шаг 3 — Разворачивайте прокси только из артефактов с закреплённым происхождением

`registerToken` — это граница безопасности, которая делает поэтапные развертывания безопасными. Он принимает только токен, чей **runtime codehash совпадает с каноническим `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 токена, а сам токен не должен быть зарегистрирован ранее.
}
```

Поскольку этот codehash **неизменяем**, прокси, собранный из любого другого артефакта, завершится неудачей по принципу fail-closed с `ProxyCodehashUnknown`. Из этого следуют два правила, и оба они обеспечиваются CI и жёсткими правилами в `CLAUDE.md`:

1. **Прокси токенов ДОЛЖНЫ быть развернуты из `rome-contracts/out`** — никогда из `contracts/out`, никогда из другой сборки. Именно эта компиляция закрепляется каноническим codehash фабрики.
2. **`bytecode_hash = "none"` и `cbor_metadata = false`** в `rome-contracts/foundry.toml` имеют критическое значение. При включённых метаданных `ArcTokenProxy` runtime *встроенный* в фабрику (`type().runtimeCode`) содержит другой CBOR/IPFS-хвост, чем отдельный артефакт `out/` из которого мастер выполняет развертывание — поэтому on-chain `registerToken` возвращает ошибку, тогда как **все тесты Foundry из исходников проходят** (Foundry встраивает обе копии, поэтому не видит несоответствия). `rome-contracts/test/ArtifactProvenance.t.sol` закрепляет это свойство; именно финансируемый прогон первоначально обнаружил проблему.

> **Зелёный in-source suite не доказывает происхождение.** Перед выпуском образа и после ЛЮБОГО изменения настроек компилятора докажите это свойство для *развернутой* фабрики (только чтение, без ключей):
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> Он хеширует ваш локальный `ArcTokenProxy` артефакт и проверяет, что этот хеш дословно присутствует внутри байткода развернутой фабрики.

Если настройки компилятора необходимо изменить, **ротация фабрики** — разверните новую, чья immutable-переменная закрепит новый codehash, и переподключите её — вместо ручной правки проверки:

```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();
    // …цена и количество должны быть положительными.
}
```

Есть и четвёртое, неявное требование, которое накладывает ограниченный актив: поскольку **каждый перевод проходит через gate allowlist**, перемещение запаса на витрину возможно только в том случае, если адрес витрины сам включён в whitelist вашего токена. Полная последовательность открытия продажи такова:

1. **Внести витрину в whitelist** в модуле whitelist вашего токена (`batchAddToWhitelist([storefront])` — см. шаг 5). Без этого перевод запаса в (2) завершится ошибкой `TransferRestricted()` после введения ограничений.
2. **Перевести запас** на адрес витрины (из квитанции infra, `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** — покупатели теперь платят wUSDC и получают RWA.

Разделение вывода стоит понимать на *общей* витрине, и оно несимметрично (`arc/roles.ts` определяет полномочия):

| Действие                                                   | Полномочие                        | Кем удерживается            |
| ---------------------------------------------------------- | --------------------------------- | --------------------------- |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | у `ADMIN_ROLE` (`onlyTokenAdmin`) | вы, эмитент                 |
| `withdrawPurchaseTokens` (поступления в wUSDC)             | у `DEFAULT_ADMIN_ROLE`            | того, кто развернул витрину |

В self-hosted-цепочке вы являетесь и тем, и другим. На общей витрине пул поступлений — это выручка каждого эмитента, поэтому вывод средств выполняется на уровне платформы — выстраивайте расчёты с учётом этого, а не исходя из предположения о единоличном контроле.

***

## Шаг 5 — Подключите собственный KYC / комплаенс

Это раздел, который клиент читает перед согласием, поэтому это код, а не описание. **Комплаенс обеспечивается самим контрактом токена** — а не внешней проверкой, фильтром секвенсора или политикой площадки. Правила следуют за токеном по любому пути исполнения, по обеим линиям кошельков. В цепочке фиксируется **результат** вашего KYC-решения в виде одного булева значения:

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol — полное состояние on-chain
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← один булев флаг на адрес. Никаких PII, никогда.
    bool transfersAllowed;                    // false = gated; переводить могут только адреса из whitelist
    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)` передаёт запись бэкенд-подписанту.

### Решение, превращающееся в запись в блокчейне

Направьте вебхук вашего провайдера на подписанта, который обладает `MANAGER_ROLE`, и при получении approve пакетно обработайте адреса:

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

// `signer` должен обладать MANAGER_ROLE в модуле allowlist ЭТОГО токена.
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// провайдер сообщил "approved" для этих адресов:
await wallet.writeContract({
  address: whitelistModule,                 // определяется ИЗ токена через getRestrictionModule
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

Определите `whitelistModule` из самого токена (`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`), а не из кэшированной проекции — нацеливание записи не в тот модуль и есть причина, по которой «approve on any asset» однажды записал в allowlist одного токена.

### Вариант с подписью кошельком (демонстрация self-service)

Для тестового развёртывания без разрешений, где посетители допускают себя сами, выдайте `MANAGER_ROLE` в `TestApprover` (`rome-contracts/src/TestApprover.sol`) чей `модуле` есть **неизменяем**. Затем `approve(address)` может вызываться кем угодно из собственного кошелька, с оплатой своего газа — он вызывает `addToWhitelist` только если адрес ещё не в списке:

```solidity
function approve(address account) external {
    IWhitelistRestrictions target = IWhitelistRestrictions(module);   // неизменно — один модуль, навсегда
    if (!target.isWhitelisted(account)) target.addToWhitelist(account);
    emit Approved(account, msg.sender);
}
```

Это «ворота входа» — они показывают посетителю реальный механизм (одобрение, превращающееся в запись allowlist в блокчейне, которую затем применяет токен) без передачи чего-либо в управление. **Никогда не выдавайте `MANAGER_ROLE` в `TestApprover` для производственного токена** — это делает allowlist этого токена доступным без разрешений.

### Когда ограничение вступает в силу и при смене провайдеров

Ограничение начинает действовать только после того, как актив **ограничен** (`setTransfersAllowed(false)`, `ADMIN_ROLE` в модуле). Пока ограничение не включено, переводы открыты — сначала соберите allowlist, затем включите ограничение. После включения перевод, в котором хотя бы одна из сторон не в списке, откатывается с типизированной ошибкой `TransferRestricted()` (`0xe827105e`).

> **Поздняя смена провайдера KYC не требует изменений в блокчейне.** Модуль allowlist, его интерфейс и хранимый им булев флаг не зависят от провайдера. Чтобы сменить вендора, направьте другой источник решений на тот же `MANAGER_ROLE` подписант — без повторного развёртывания, без миграции, без изменения состояния. Блокчейн никогда не знал, какой вендор сделал вызов, только итог.

***

## Шаг 6 — Откройте тот же актив для кошельков Solana

Те же три контракта доступны из кошелька Solana **без моста и без второго токена**. У нативного пользователя Solana вообще нет EVM-ключа; вместо этого его EVM-идентичность является *синтетической*, полученной в блокчейне из фактического подписанта Solana в транзакции:

```
synthetic EVM address = 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 в allowlist по его синтетическому адресу.** Для уровня комплаенса синтетический адрес — обычный адрес: вставьте публичный ключ Solana пользователя, выведите синтетический адрес и `batchAddToWhitelist([synthetic])`. Один allowlist охватывает оба мира кошельков.
2. **Подготовьте signing PDA один раз, до их первой транзакции.** PDA внешнего полномочия, которое авторизует подпись синтетического адреса, должно существовать заранее, и его создание — это вызов в EVM-канале (`create_pda`) — пользователь только с Solana не может сделать это сам, поэтому это обеспечивает эмитент. Подготовка — это `external_auth`, а не ленивая инициализация.
3. **Отправляйте через библиотеку канала, а не вручную.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`) формирует `DoTxUnsigned` инструкцию — *неподписанный* payload EIP-1559, авторизованный подписью Solana, — и обрабатывает четыре вещи, в которых вручную собранная транзакция обычно ошибается:
   * **Обнаружение аккаунтов** через `rome_emulateCallAccounts`, которая определяет записываемость каждого аккаунта. Значение `value` **должны** должно быть передано в discovery, иначе перевод value пометит PDA баланса получателя как read-only и завершится откатом в блокчейне.
   * **Бюджет вычислений** — `DoTxUnsigned` требует **кадра heap на 250 КБ** и **1,35 млн CU** лимит (EVM Rome превышает стандартные 32 КБ / 200 K); при этом остаётся около 50 K запаса под потолком Solana в 1,4 млн.
   * **PDA кошелька сокровищницы (fee)**, добавляемый как writable — discovery его не включает.
   * **Альтернативный вариант v0 + lookup-table.** Когда аккаунты вызова не помещаются в унаследованную оболочку транзакции размером 1232 байта, клиент повторно отправляет её как транзакцию v0 через новую таблицу адресов lookup. (По текущим измерениям покупка в storefront помещается в унаследованную оболочку примерно на 1 061 из 1232 байт; у держателя, для которого ещё нужно создать associated token account, лимит может быть превышен.)

Пользователь является **плательщиком комиссии** и платит lamports (SOL) из своего кошелька Solana; управляемый ими синтетический адрес хранит активы и никогда не хранит SOL. После каждой отправки по каналу запрашивайте EVM nonce перед следующей — индексатор немного отстаёт от подтверждения Solana.

> **Канал предназначен ТОЛЬКО для атомарных операций.** A `DoTxUnsigned` — это одна транзакция EVM, выполняемая внутри одной транзакции Solana атомарной VM Rome. Итеративная VM (одна EVM-экзекуция, распределённая по нескольким транзакциям Solana)  **пока недоступна через этот канал**. Вызов, который не помещается атомарно, не может использовать этот канал вовсе — решение состоит в меньшем вызове, а не в запасном варианте. Обратите внимание: fallback v0+ALT — это более крупная *оболочка*, а не другое исполнение; это всё ещё одна атомарная транзакция.

**Почему RWA не может выйти за пределы периметра.** Актив находится в состоянии EVM — его балансы хранятся внутри аккаунтов программы Rome EVM. Там нет **SPL mint** этого, и нет нативного для Solana объекта, который можно перемещать; примитивы перемещения активов в канале работают с SPL token accounts, которых у баланса в storage EVM нет. Любое перемещение, которое может выразить подпись Solana, — это вызов контракта токена, а он проходит через allowlist gate — тестовый набор подтверждает, что перевод на адрес вне allowlist откатывается одинаково, независимо от того, подписан ли он ключом EVM или Solana. Денежная часть (wUSDC) — сознательное исключение: она основана на SPL и *может* быть выведена на собственный token account пользователя.

***

## Шаг 7 — Узнайте, кто обладает полномочиями после развёртывания

`ArcToken.initialize` выполняется из конструктора proxy (транзакция 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)` вместо того чтобы читать контракт самой роли, поскольку четыре функции storefront и обе функции фабрики ограничены полномочием, которое находится в *другом* контракте.

***

## Шаг 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);
```

Ключевые механики для корректного запуска:

* **Вся `сумма извлекается у вызывающего только при` окне `startIndex == 0` окне** (`safeTransferFrom(msg.sender, this, totalAmount)`). Предварительно одобрите эту сумму для токена до первого окна; последующие окна выплачиваются из уже имеющегося баланса.
* **`nextIndex` сбрасывается до `0` по завершении обхода.** Повторяйте цикл `distributeYieldWithLimit(total, nextIndex, …)` пока `nextIndex` не вернётся `0`.
* **Доли держателей, для которых доход ограничен, остаются в контракте токена** — они пропускаются, а не перераспределяются.

> **В Rome практический предел составляет `maxHolders = 1` за транзакцию** — тот же лимит в 62 аккаунта из шага 2 — поэтому доход обходится по одному держателю за транзакцию (по измерениям около 318 K газа на держателя). Каждый держатель в allowlist, будь то EVM или нативный Solana, получает пропорционально; эталонный скрипт: `measure-yield-batch.ts` а шаг дохода в funded smoke проверяет это end-to-end.

```bash
# Форма-ссылка из USAGE.md: обход одного держателя на транзакцию, пока nextIndex не сбросится до 0
distributeYieldWithLimit(total, offset, 1)
```

***

## Шаг 9 — Проверьте, что вы развернули

Ничто в Rome не отправляет исходники автоматически, поэтому верификацию нужно запускать вручную. После любого развёртывания — инфраструктуры или нового токена — запустите read-only, без ключей, идемпотентный верификатор (уже верифицированные контракты пропускаются, поэтому повторный запуск после выпуска токена проверит только новые):

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

Ожидается **три** наличие контрактов на токен (proxy + оба модуля) плюс инфраструктуры, и сообщает, что не удалось верифицировать, вместо того чтобы проваливать развёртывание. При ручном выполнении один раз было пропущено семь контрактов.

Регистрация актива в реестре Rome рассматривает верификацию как **ворота**, а не как украшение (`register-asset.ts`): токен получает свою `apps/arc/<chain>.json` запись — с `standard: arc-permissioned-erc20` и `transferRestriction` именующую его allowlist gate — только когда все три его контракта верифицированы в Sourcify. Эта запись получает факты из цепочки (токен, развёрнутый мастером, не имеет зафиксированного receipt) и записывает только локальную копию реестра; доведение PR до мерджа — задача человека.

```bash
CHAIN_ID=200010 npx tsx register-asset.ts <tokenAddress>        # верифицировать + подготовить запись реестра
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # предварительный просмотр только для чтения
```

Для сквозного подтверждения всего цикла — создать, добавить в allowlist, включить ограничение, продать по обоим каналам, распределить доход — запустите funded smoke на любой цепочке:

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

***

## Шаг 10 — Знайте, что остаётся вне блокчейна

Модель доверия намеренно прозрачна, и разработчик должен так же представлять её своим пользователям:

* **Флаг allowlist — это ваше решение вне блокчейна.** KYC/AML выполняются в вашем процессе — ваши провайдеры, ваши правила. Блокчейн фиксирует и применяет только *результат*, `isWhitelisted(addr)`.
* **Единственное состояние в блокчейне — один булев флаг на адрес.** Никаких PII, никаких документов, никаких заявлений об идентичности, никаких аттестаций в блокчейне, никакого криптографического доказательства принадлежности адреса. Это модель Arc, используемая в Plume; она меняет более тяжёлые механизмы стандарта идентичности в блокчейне (например, ERC-3643) на более лёгкую механику и доверие, удерживаемое эмитентом.
* **Восстановление — это полномочие эмитента, а не аварийный обход.** Утерянный кошелёк — EVM или Solana — восстанавливается путём добавления заменяющего адреса в allowlist и, при необходимости, использованием mint/burn/upgrade в рамках вашего юридического процесса (в Arc нет встроенного примитива принудительного перевода). Поэтому хранение ключей эмитентом является частью комплаенс-позиции.
* **Нет релея, нет поверхности хранения.** Пользователи сами держат свой gas в обоих каналах. Сервис fee-payer означал бы хранение ключей — это поверхность, которую продукт RWA не должен добавлять.

***

## Справка: инструменты

Каждый скрипт запускается из `scripts/` после `npm install`, при этом `PRIVATE_KEY`, `CHAIN_ID`, и `REGISTRY_ROOT` экспортируются.

| Скрипт                          | Что он делает                                                                         |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| `deploy-infra.ts`               | Однократно для каждой цепочки: четыре общих контракта, настроенные для wUSDC.         |
| `create-token.ts`               | Поэтапный мастер — восемь транзакций, один токен.                                     |
| `rotate-factory.ts`             | Разверните новую фабрику после изменения компилятора/артефакта; перенастройте связи.  |
| `verify-artifact-provenance.ts` | Докажите, что ваш артефакт proxy принимается развёрнутой фабрикой (только чтение).    |
| `verify-deployments.ts`         | Верифицируйте все три контракта на токен + инфраструктуру в Sourcify (только чтение). |
| `register-asset.ts`             | Сначала проверьте gate, затем подготовьте compliance-запись в реестре.                |
| `measure-yield-batch.ts`        | Повторно измерьте предел дохода на транзакцию в цепочке.                              |
| `smoke.ts`                      | Финансируемый end-to-end набор для обоих каналов.                                     |
| `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/ru/prilozheniya-na-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.
