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

# Bloom에서 구축하기

이 가이드는 한 가지 질문에 답합니다: **빌더가 이를 직접 어떻게 수행하는가?** Solidity 실물 자산 계약을 가져와, 이를 권한형 토큰으로 Rome 체인에 배포하고, 브리지나 두 번째 토큰 없이 Solana 지갑과 EVM 지갑 모두에 열어 두며, 자체 KYC/컴플라이언스 판단을 연결한다.

대상 독자는 이 접근 방식을 평가하거나 도입하려는 개발자입니다. Bloom은 전체에 걸친 작업 예시이며, 그 실물 자산 **ARCV ("Mineral Vault I")** Hadrian (`200010`)에서의 실제 사례이며, 아래 모든 단계의 실제 예입니다. 그리고 그 배포 영수증 (`deployments/200010.tokens/ARCV.json`)에는 이 방식이 생성하는 정확히 세 개의 계약과 여덟 개의 트랜잭션이 기록되어 있습니다.

여기에는 자산 계약을 수정하는 내용이 없습니다. Bloom은 Plume의 Arc 프레임워크 **수정 없이** (vendored tree는 `contracts/`, `plumenetwork/contracts` 의 핀에서 바이트 수준까지 동일한 `NOTICE`); Rome 쪽 계약은 오직 `ArcTokenFactoryV2`뿐이며, 여기에 함수 하나가 추가됩니다. 작업은 *어떻게* 배포하고 *어떻게* 계약을 운용하는 데 있고, 계약을 바꾸는 데 있지 않습니다.

> **방법을 한눈에.** 권한형 토큰은 **세 개의** 배포된 계약입니다. 그것은 **조각별로** — 트랜잭션당 하나의 계약 — 배포되는데, 단일 트랜잭션 팩토리 호출은 Rome의 트랜잭션당 계정 한도를 초과하기 때문입니다. 그 프록시는 **출처가 잠긴 아티팩트** 에서 와야 하며, 그렇지 않으면 온체인 등록이 닫힌 상태로 실패합니다. 컴플라이언스는 주소당 하나의 **불리언**으로, 귀하의 KYC 프로세스가 승인한 사람이 작성합니다. 그리고 동일한 배포는 Solana 지갑에서도 **가상 송신자** 를 통해 도달할 수 있습니다. 온체인에서 파생되며, 브리지도 래핑 토큰도 두 번째 허용목록도 없습니다.

## 사전 요구 사항

> **시작하기 전에.** 이 가이드가 사용하는 발행자 도구는 아직 공개되지 않았습니다. 여기에는 스크립트 (`deploy-infra.ts`, `create-token.ts` 및 나머지), `contracts/` 및 `rome-contracts/` 트리, 그리고 그 안에서 이름이 언급된 배포 영수증이 포함됩니다. 발행자는 Rome와 온보딩하면서 이 도구를 받습니다. 그 전까지는 이 가이드를 방법과 각 스크립트가 하는 일에 대한 설명으로 읽으세요.

참고용 도구는 `scripts/` 디렉터리에 있습니다(온보딩 시 제공됨; 의존성은 `npm install`로 설치). 모든 명령은 하드코딩 대신 레지스트리에서 체인 정보를 가져오므로, 다른 Rome 체인으로 바꾸는 것은 코드 변경이 아니라 환경 변경입니다:

```bash
export PRIVATE_KEY=…       # 대상 체인에서 자금이 있는 배포자/발행자 키
export CHAIN_ID=200010     # 체인을 선택
export REGISTRY_ROOT=…     # Rome 체인 레지스트리의 체크아웃
# Solana 경로 흐름에는 다음도 필요합니다:
export SOLANA_KEYPAIR=…    # 자금이 있는 Solana 키페어 JSON 경로(수수료 지불자)
```

Rome 체인의 가스는 USDC 표시입니다. 전체 배포 절차는 devnet에서 대략 10\~15개의 네이티브 단위가 듭니다. 키는 환경에서만 가져오며, 저장소에서 가져오지 않습니다. 배포하기 전에 계약을 한 번 빌드하세요. 아티팩트는 **반드시** 와야 합니다 `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`                     | 토큰 자체 — 공유 `ArcToken` 구현을 가리키는 UUPS 프록시입니다. 잔액, 보유자, 역할이 여기에 있습니다. |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | 허용목록 모듈입니다. 주소당 하나의 불리언; 자산이 게이팅되는 동안 모든 전송에 대해 강제됩니다.             |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | 수익 모듈입니다. 보유에는 손대지 않고 주소를 수익 분배에서 제외합니다.                           |

**공유되는, 체인당 하나씩의 네 개 계약** (배포 주체: `deploy-infra.ts`):

| 계약                   | 역할                                   |
| -------------------- | ------------------------------------ |
| `RestrictionsRouter` | 모듈 유형 레지스트리 — 체인 연결, ops가 한 번 설정합니다. |
| `ArcTokenFactoryV2`  | 등록 + 표준 프록시 코드해시(2\~3단계).            |
| `ArcToken` (구현체)     | 모든 토큰이 프록시로 연결하는 공유 재사용 자산 로직입니다.    |
| `ArcTokenPurchase`   | 모든 토큰과 발행자가 공유하는 하나의 판매 창구(4단계).     |
| wUSDC                | 현금 측면 — 수익 통화와 판매-구매 통화.             |

전송 제한과 수익 제한은 **의도적으로 분리된 모듈**입니다. 보유자는 자산은 유지한 채 소득에서만 제한될 수 있는데, 이것이 실제 제재와 법원 명령이 작동하는 방식입니다.

이 "세 개의 계약" 사실은 검증에서 핵심입니다. `verify-deployments.ts` 및 `register-asset.ts` 둘 다 토큰이 세 개의 배포된 계약으로 해석되기를 기대합니다. 한 번 토큰을 수동 검증할 때 체인 전체에서 일곱 개를 놓쳤습니다(모든 수익 모듈과 세 개의 허용목록 모듈).

라이프사이클에는 세 단계와 하나의 일방통행 문이 있습니다:

```
draft     프록시가 활성화됨, 모듈 연결 없음 — 아무 것도 전송을 게이트하지 않음
  ↓
ungated   모듈 연결됨, transfersAllowed = true   (WhitelistRestrictions.initialize가 이를 설정)
  ↓  공급량 민팅, 허용목록 구축               [되돌릴 수 있음]
gated     setTransfersAllowed(false)                [일방통행 문 — 공급량이 최종 확정됨]
```

게이팅은 공급량에 대해 일방향입니다. 민팅과 소각이 `address(0)` 를 상대 당사자로 사용하고, `address(0)` 는 절대 허용목록에 올릴 수 없기 때문입니다. 따라서 일단 게이트되면 새로운 공급량은 더 이상 민팅될 수 없습니다. 이것이 권한형 자산이 보유자에게 제공하는 보증입니다.

***

## 2단계 — 한 번의 트랜잭션이 아니라 조각별로 배포하기

Arc는 원샷 `createToken` 를 제공합니다. 이건 프록시와 두 모듈을 배포하고 단일 호출로 연결합니다. **Rome에서는 사용할 수 없습니다.** 그 트랜잭션은 **77개의 Solana 계정 잠금**이 필요하고, Rome의 트랜잭션당 한도는 **62개 잠금**입니다. 튜닝을 해도, 최적화 패스를 적용해도 절대 맞지 않습니다.

그래서 배포는 **조각별로**: 각 계약을 별도의 트랜잭션으로 배포하고, 토큰은 `ArcTokenFactoryV2.registerToken` 대신 `createToken`모놀리식한 단일 호출

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

참고 구현은 `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
# 체인당 한 번만(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)이 storefront의 `enableToken` 및 팩토리 매개 업그레이드를 가능하게 하는 것입니다.

> **이를 다시 모놀리식으로 "최적화"하지 마세요.** 왕복을 줄이려고 이 호출들을 하나의 트랜잭션으로 묶으면, 조각별 경로가 피하려는 바로 그 62-잠금 오버플로가 다시 발생합니다. 같은 한도 때문에 수익 분배도 한 보유자씩 한 트랜잭션으로 진행합니다(8단계). 호출이 `계정이 너무 많음: 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) 토큰은 팩토리가 아는 토큰이어야 합니다 — 이것이 2단계의 registerToken으로 확보한 것입니다:
    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;   // ← 주소당 하나의 불리언. 절대로 PII 없음.
    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 시 부여되며 **전체 suite 어디에서도 검사되지 않습니다** (`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`를 보유한 서명자에게 연결하고, 승인 시 주소들을 일괄 처리하세요:

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

// `signer`는 이 토큰의 허용목록 모듈에서 MANAGER_ROLE을 보유해야 합니다.
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)`), 캐시된 프로젝션에서는 절대 가져오지 마세요 — 잘못된 모듈에 쓰기를 시도하는 것이 바로 "모든 자산에 대한 승인"이 한때 한 토큰의 허용 목록에 기록되었던 방식입니다.

### 지갑 서명 변형(셀프 서비스 데모)

방문자가 스스로 가입하는 권한 없는 테스트 배포에서는 다음을 부여하세요 `MANAGER_ROLE` 에 `TestApprover` (`rome-contracts/src/TestApprover.sol`)의 `모듈` 인 것을 선택하세요 **불변**. 그런 다음 `approve(address)` 는 누구나 자기 지갑으로, 자기 가스를 내고 호출할 수 있습니다 — 호출 시 `addToWhitelist` 주소가 아직 목록에 없을 경우에만:

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

이것이 "로그인 게이트"입니다 — 방문자에게 실제 메커니즘(승인이 온체인 허용 목록 항목이 되고, 그 다음 토큰이 이를 강제하는 방식)을 보여 주면서도 어떤 자산도 보관하지 않습니다. **절대 부여하지 마세요 `MANAGER_ROLE` 에 `TestApprover` 프로덕션 토큰에서** — 그러면 해당 토큰의 허용 목록이 무권한화됩니다.

### 게이트가 적용될 때, 그리고 제공자를 교체할 때

게이트는 자산이 다음 상태가 된 후에만 제한합니다 **게이트 적용** (`setTransfersAllowed(false)`, `ADMIN_ROLE` 를 모듈에 설정합니다). 게이트가 적용되지 않은 동안에는 전송이 열려 있습니다 — 먼저 허용 목록을 만든 다음 게이트를 거세요. 게이트가 적용되면 양쪽 중 어느 한쪽이라도 목록에 없을 경우 전송은 타입화된 오류로 되돌아갑니다 `TransferRestricted()` (`0xe827105e`).

> **나중에 KYC 제공자를 바꿔도 온체인 변경은 필요 없습니다.** 허용 목록 모듈, 그 인터페이스, 그리고 저장하는 불리언은 제공자에 구애받지 않습니다. 공급업체를 바꾸려면 다른 결정 소스를 같은 곳에 연결하세요 `MANAGER_ROLE` 서명자에 연결하면 됩니다 — 재배포도, 마이그레이션도, 상태 변경도 없습니다. 체인은 어떤 공급업체가 호출했는지 알지 못하고, 결과만 압니다.

***

## 6단계 — 같은 자산을 솔라나 지갑에도 개방하기

같은 세 개의 컨트랙트는 솔라나 지갑에서도 접근할 수 있습니다 **브리지도 없고, 두 번째 토큰도 없습니다**. 솔라나 네이티브 사용자는 어디에도 EVM 키가 없으며, 대신 그들의 EVM 신원은 *합성*, 트랜잭션의 실제 솔라나 서명자로부터 온체인에서 파생됩니다:

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

Rome EVM 프로그램은 이것을 서명자 자체에서 도출합니다(`do_tx_unsigned::derive_sender`), 따라서 위조할 수 없고 이에 대한 secp256k1 개인키도 존재하지 않습니다 — **이 주소를 작동시킬 수 있는 것은 오직 솔라나 서명뿐입니다**. 기준 파생 방식은 `arc/materialise/solana/identity.ts` (`syntheticAddress`), 온체인 규칙과 바이트 호환됩니다:

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

**빌더가 이 레인을 지원하기 위해 하는 일** — 다음을 주의하세요 *그 어떤 것도 컨트랙트 변경이 아닙니다*:

1. **솔라나 사용자를 그들의 합성 주소로 허용 목록에 추가합니다.** 컴플라이언스 계층에서 합성 주소는 일반 주소와 같습니다: 사용자의 솔라나 공개키를 붙여 넣어 합성 주소를 도출하고 `batchAddToWhitelist([synthetic])`. 하나의 허용 목록이 두 지갑 세계를 모두 포괄합니다.
2. **서명용 PDA는 첫 가치 이동 호출 전에 한 번만 준비하세요.** 합성 주소를 위해 서명하는 외부 권한 PDA가 먼저 존재해야 하며, 이를 생성하는 것은 한 `create_pda` 번의 호출입니다. 양쪽 어느 쪽에서든 보낼 수 있습니다: 사용자의 솔라나 지갑이 레인을 통해 보내거나(SDK의 `buildCreatePdaCall`, 이후 `isSyntheticProvisioned` 가 false를 반환한 뒤), 또는 발행자가 EVM 키로 보낼 수 있습니다. Bloom의 발행자 도구는 현재 보유자 온보딩의 일부로 이를 전송하며, 앱은 아직 사용자의 첫 동작에서 이를 수행하지 않으므로 보유자의 첫 구매 전에 미리 준비하세요. SDK 호출로 어떻게 하는지는 [이중 레인 앱](/ko/developer-guides/dual-lane-app.md#one-time-activate-provision-the-synthetic).
3. **수작업이 아니라 레인 라이브러리를 통해 제출하세요.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`) **프록시가 구성한 Solana v1 트랜잭션** (`rome_composeUnsignedAtomicV1`)이며 주소 조회 테이블이 없고, 레인 라이브러리는 더 오래된 형식으로 되돌아가지 않고 안전하게 실패합니다. 5단계의 현금 유입 동작은 앱이 직접 구성하는 별도의 솔라나 트랜잭션이지, 구성된 컨트랙트 호출이 아닙니다.
   * **계정 발견과 컴퓨트 예산은 프록시의 역할입니다.** 프록시는 호출을 에뮬레이션하고 각 계정의 쓰기 가능 여부를 결정합니다(호출의 `value` 은 에뮬레이션하는 내용의 일부이므로 전달하세요),
   * **그리고 반환하는 트랜잭션에 컴퓨트 및 힙 예산을 설정합니다. 여러분의 코드 어디에서도 컴퓨트 예산 명령을 만들거나 수수료 풀 계정을 추가하지 않습니다.** 지갑은 먼저 읽을 수 있는 승인 메시지에 서명합니다(*Rome 승인* 와 서명되지 않은 EIP-1559 페이로드의 해시를 더한 것; Wallet Standard `solana:signMessage`), 프록시가 호출을 구성하기 전에 이를 검증합니다. 그런 다음 프록시가 반환한 v1 트랜잭션에 서명합니다(`solana:signTransaction` 원시 와이어에서). `signAuthHash` 및 `signV1Wire`, 결코 하나로 합친 메서드가 아닙니다. 그 위에 구축하기 전에 지갑이 v1 트랜잭션에 서명하는지 확인하세요: Solflare는 오늘 devnet에서 가능하고 Phantom은 아직 아닙니다.
   * **앱이 서명된 트랜잭션을 직접 솔라나 RPC로 보냅니다.** 프록시는 구성만 합니다; 원자적 호출을 브로드캐스트하지는 않습니다.
   * **서명을 요청하기 전에 검증하세요.** 레인 라이브러리는 구성된 메시지를 파싱하고, 트랜잭션 프롬프트 전에 형식이 잘못된 메시지를 거부합니다.
4. **첫 wUSDC 지출 전에 1회용 래퍼 권한 부여를 먼저 하세요.** wUSDC는 사용자의 토큰을 그 사용자의 SPL 대리인으로서 이동시키므로, 한 번도 권한이 부여되지 않은 합성 주소는 첫 wUSDC 전송에서 SPL 오류를 받습니다 `0x4` 를 받습니다. 읽어 보세요 `isEnabled(user)` 래퍼에서, 그리고 그것이 false일 때는 앞에 추가하세요 `approve_spl(wrapper, 2^64-1, mint)` Helper 프리컴파일에 대한 호출을 실행의 첫 번째 단계로. 이는 어느 레인이든 일반 호출이며, 지갑당 한 번만 하면 됩니다. 참고: [지갑당 1회 권한 부여](/ko/core-concepts/token-interop.md#one-time-grant-per-wallet).
5. **그것을 소비하는 호출 전에 현금을 먼저 들여오세요.** 합성 주소는 정지 상태에서 아무것도 보유하지 않습니다. 호출이 소비하는 양보다 보유한 wUSDC가 적으면, 먼저 지갑 자체의 USDC 토큰 계정에서 부족분을 옮기세요(합성의 토큰 계정을 멱등적으로 생성한 다음, Rome 프로그램의 activate-token-account 명령으로 EVM을 실행하지 않고 토큰을 이동합니다). 이는 하나의 프롬프트로 서명하는 단일 솔라나 트랜잭션이며, 합성이 이미 충분히 보유하고 있으면 건너뜁니다.
6. **원자적 컴포저가 거부하는 것만 릴레이하세요.** 하나의 솔라나 트랜잭션에 들어가지 않는 호출(약 140만 컴퓨트 유닛, 64개 계정, 4KB)은 컴포저가 다음과 같이 거부합니다 *외부 원자 계획에는 반복적 구성이 필요합니다*. 바로 그 거부가 발생했을 때만, 그 이전에는 절대 없이, 다음을 호출하세요 `rome_relayUnsignedV1` 같은 승인 서명으로: 프록시가 이를 구성하고 제출하며, 솔라나 수수료를 지불하고, EVM 트랜잭션 해시로 응답합니다(솔라나 서명 아님). 합성 주소는 체인의 네이티브 가스를 wei로 지불하므로, 잔액이 다음보다 부족할 때는 `gasLimit x maxFeePerGas`, 먼저 다음으로 충전하세요 `rome_composeExternalDepositV1` (지갑이 서명한 v1 트랜잭션; 금액은 10^12 wei의 배수여야 함) — 실제로는 지갑당 한 번만 하면 됩니다. revert, 네트워크 실패, 서명 거부와 같은 다른 오류는 릴레이되지 않고 다시 던져집니다. 자세한 내용은 [릴레이된 호출 및 장시간 호출](/ko/developer-guides/call-evm-from-solana.md#relayed-calls-and-long-calls).

사용자는 **수수료 납부자** 이며 직접 호출 시 자신의 솔라나 지갑에서 lamports(SOL)를 지불합니다. 그들이 제어하는 합성 주소는 자산을 보유하며 SOL은 절대 보유하지 않습니다. 각 레인 전송 후에는 다음 호출 전에 EVM 논스를 확인하세요 — 인덱서가 솔라나 확인보다 약간 뒤처지기 때문에, 그렇지 않으면 다음 호출이 체인이 이미 사용한 논스를 기반으로 구성될 수 있습니다.

> **원자적으로 먼저, 거부 시 릴레이.** 모든 호출의 기본값은 사용자의 지갑이 서명한 하나의 솔라나 트랜잭션 안에서 실행되는 하나의 EVM 트랜잭션입니다. 릴레이는 들어가지 않는 호출에 대한 예외이지, 선호에 따라 고를 수 있는 두 번째 경로가 아닙니다: 직접 호출은 두 번의 프롬프트가 필요하고, 릴레이 호출은 한 번이며, 릴레이 호출은 lamports가 아니라 Rome 가스로 지불됩니다. 다단계 여정(승인 후 구매)은 여러 개의 완전한 호출이며, 각각이 별도 트랜잭션이지 트랜잭션 간에 나뉜 하나의 실행이 아닙니다.

**RWA가 경계 밖으로 새어 나갈 수 없는 이유** 자산은 EVM 상태에 상주합니다 — 잔액은 Rome EVM 프로그램 계정 내부의 저장소입니다. 여기에는 **SPL 민트가 없습니다** 그에 해당하는 것도 없고, 이동할 솔라나 네이티브 자산도 없습니다. 레인의 자산 이동 프리미티브는 SPL 토큰 계정에서 작동하는데, EVM 저장 잔액에는 그런 계정이 없습니다. 솔라나 서명으로 표현할 수 있는 모든 이동은 토큰 컨트랙트로의 호출이며, 이 컨트랙트가 허용 목록 게이트를 실행합니다 — 자금이 있는 테스트는 EVM 키로 서명하든 솔라나 키로 서명하든 허용 목록에 없는 주소로의 전송이 동일하게 되돌아감을 검증합니다. 현금 레그(wUSDC)는 의도된 예외입니다: SPL 기반이며 *은* 사용자의 자체 토큰 계정으로 스윕됩니다.

***

## 7단계 — 배포 후 누가 권한을 가지는지 파악하기

`ArcToken.initialize` 는 프록시 생성자(tx 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 시 부여되지 않습니다.** 초기 공급량은 내부에서 민트됩니다 `초기화` (내부의 `_mint`, 역할은 필요하지 않지만), 그러나 *이후의* `mint` 또는 `burn` 는 발행자가 먼저 스스로에게 그 역할을 부여해야 합니다. 이는 가능합니다. 그들이 다음을 보유하고 있기 때문입니다 `DEFAULT_ADMIN_ROLE` (모든 역할의 관리자 — 아무것도 다음을 호출하지 않기 때문입니다 `_setRoleAdmin`, 따라서 `DEFAULT_ADMIN_ROLE` 모든 역할을 관리합니다).
* **`UPGRADER_ROLE` 는 팩토리에는 절대 부여되지 않습니다.** 팩토리 중재 업그레이드(`ArcTokenFactoryV2.upgradeToken`)에는 **팩토리 자체가** 보유해야 합니다 `UPGRADER_ROLE` 토큰에서 이를 보유해야 합니다 — 자신에게 부여해도 아무 효과가 없습니다. 그 부여는 팩토리가 되돌릴 수 없는 일방통행문이므로 선택 사항입니다: 팩토리 중재 업그레이드를 원한다면 `grantRole(UPGRADER_ROLE, factory)` 를 명시적으로 하세요. 그렇지 않으면 발행자가 토큰을 직접 업그레이드합니다.

전체 21개 권한 모델(6개 컨트랙트에 걸친 11개 역할 이름 — 같은 이름이라도 컨트랙트마다 다른 권한입니다)은 다음에 문서화되어 있습니다 `arc/roles.ts`. `rolesRequiredFor(contract, functionName)` 역할 자체의 컨트랙트를 읽는 대신 이 함수를 사용하세요. 왜냐하면 네 개의 스토어프론트 함수와 두 개의 팩토리 함수는 다음에 존재하는 권한에 의해 게이트되기 때문입니다 *다른* 컨트랙트.

***

## 8단계 — 수익 배분하기

토큰은 wUSDC로 수익을 지급합니다(다음에 고정 `초기화`, 다음을 통해 변경 가능 `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`.
* **수익이 제한된 보유자의 몫은 토큰 컨트랙트에 남습니다** — 이는 재분배되지 않고 건너뜁니다.

> **Rome에서 실제 한도는 `maxHolders = 1` 트랜잭션당** — 2단계와 같은 62개 계정 한도입니다 — 따라서 수익은 트랜잭션당 보유자 한 명씩 순회됩니다(보유자당 약 318K 가스로 측정됨). 허용 목록에 있는 모든 보유자, EVM이든 솔라나 네이티브든, 비례 배분을 받습니다; 기준 스크립트는 `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
```

이는 다음을 기대합니다 **세 개의** 토큰당 컨트랙트(프록시 + 두 모듈)와 인프라를 대상으로 하며, 배포를 실패시키는 대신 검증하지 못한 항목을 보고합니다. 이를 수동으로 한 번 수행했을 때 7개 컨트랙트를 놓쳤습니다.

자산을 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>        # verify + prepare the registry record
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # read-only preview
```

전체 흐름 — 생성, 허용 목록 등록, 게이트 적용, 두 레인 모두에서 판매, 수익 분배 — 의 엔드투엔드 증명을 위해 어떤 체인이든 대상으로 자금이 투입된 smoke 테스트를 실행하세요:

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

***

## 10단계 — 오프체인에 남는 것을 이해하기

신뢰 모델은 의도적으로 솔직하며, 빌더도 자신의 사용자에게 이를 그렇게 설명해야 합니다:

* **허용 목록 플래그는 오프체인에서 내리는 결정입니다.** KYC/AML은 귀하의 프로세스 — 귀하의 공급업체, 귀하의 규칙 — 에서 이루어집니다. 체인은 오직 다음만 기록하고 집행합니다 *결과*, `isWhitelisted(addr)`.
* **온체인 상태는 주소당 불리언 하나뿐입니다.** PII도, 문서도, 신원 주장도, 온체인 증명도, 주소의 소유자가 누구인지에 대한 암호학적 증명도 없습니다. 이것이 Plume에서 사용되는 Arc의 모델입니다; 온체인 신원 표준(예: ERC-3643)의 무거운 이전 방식을 더 가벼운 메커니즘과 발행자 보유 신뢰로 대체합니다.
* **복구는 발행자의 권한이지 탈출구가 아닙니다.** 분실된 지갑 — EVM이든 Solana든 — 은 대체 주소를 허용 목록에 추가하고, 필요하다면 자체 법적 절차에 따라 mint/burn/upgrade를 사용해 복구합니다(Arc에는 강제 전송 프리미티브가 내장되어 있지 않습니다). 따라서 발행자의 키 보관은 컴플라이언스 태세의 일부입니다.
* **Bloom 자체의 릴레이어도 없고, 보관 표면도 없습니다.** 사용자는 양쪽 레인 모두에서 자신의 가스를 직접 보유합니다. 앱이 운영하는 fee-payer 서비스는 키를 보유한다는 뜻이며 — RWA 제품이 추가해서는 안 되는 표면입니다. 솔라나 레인에서의 유일한 릴레이는 한 트랜잭션에 너무 큰 호출을 위한 체인 프록시의 것이며, 사용자도 여전히 자신의 서명으로 승인하고 자신의 Rome 가스로 지불합니다.

***

## 참조: 도구

모든 스크립트는 도구의 `scripts/` 디렉터리에서 실행되며, `PRIVATE_KEY`, `CHAIN_ID`, 그리고 `REGISTRY_ROOT` 가 내보내집니다.

| 스크립트                            | 하는 일                                             |
| ------------------------------- | ------------------------------------------------ |
| `deploy-infra.ts`               | 체인당 1회: wUSDC에 연결된 네 개의 공유 컨트랙트.                 |
| `create-token.ts`               | 단계별 마법사 — 8개의 트랜잭션, 하나의 토큰.                      |
| `rotate-factory.ts`             | 컴파일러/아티팩트가 바뀌면 새 팩토리를 배포하고 다시 연결하세요.             |
| `verify-artifact-provenance.ts` | 배포된 팩토리가 프록시 아티팩트를 수용하는지 증명합니다(읽기 전용).           |
| `verify-deployments.ts`         | Sourcify에서 토큰당 세 개의 컨트랙트 + 인프라를 모두 검증합니다(읽기 전용). |
| `register-asset.ts`             | 게이트 검증 후, 레지스트리 컴플라이언스 기록을 준비하세요.                |
| `measure-yield-batch.ts`        | 체인에서 트랜잭션당 수익 한도를 다시 측정합니다.                      |
| `smoke.ts`                      | 두 레인 전반에 걸친 자금 투입 엔드투엔드 스위트.                     |
| `sweep-synthetic-native.ts`     | 합성 계정에서 네이티브 가스를 회수합니다(테스트 정리).                  |

더 깊은 배경 정보는 이 가이드와 함께 있습니다: `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 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/ko/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.
