> 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/de/apps-auf-rome/bloom/building.md).

# Auf Bloom aufbauen

Dieser Leitfaden beantwortet eine Frage: **Wie macht ein Builder das selbst?** Nimm einen Solidity-Real-World-Asset-Vertrag, setze ihn auf einer Rome-Chain als berechtigtes Token auf, öffne ihn für Solana-Wallets ebenso wie für EVM-Wallets ohne Bridge und ohne zweites Token, und binde deine eigene KYC-/Compliance-Entscheidung ein.

Die Zielgruppe ist ein Entwickler, der den Ansatz evaluiert oder übernimmt. Bloom ist durchgehend das durchgearbeitete Beispiel — sein Live-Asset **ARCV ("Mineral Vault I")** auf Hadrian (`200010`) ist eine reale Instanz aller Schritte unten, und sein Deploy-Receipt (`deployments/200010.tokens/ARCV.json`) dokumentiert die genau drei Verträge und acht Transaktionen, die diese Methode erzeugt.

Nichts hier verändert die Asset-Verträge. Bloom betreibt Plumes Arc-Framework **unverändert** (der vendorte Baum in `contracts/`, byte-identisch zu `plumenetwork/contracts` am Pin in `NOTICE`); der einzige Vertrag auf Rome-Seite ist `ArcTokenFactoryV2`, der eine Funktion hinzufügt. Die Arbeit steckt in *wie* du die Verträge bereitstellst und *wie* du die Verträge steuerst — nicht darin, sie zu ändern.

> **Die Methode auf einen Blick.** Ein berechtigtes Token ist **drei** bereitgestellte Verträge. Es wird **stückweise** — ein Vertrag pro Transaktion — bereitgestellt, weil ein Factory-Aufruf in einer Transaktion das Account-Limit von Rome pro Transaktion überschreitet. Seine Proxys müssen aus **herkunftsverriegelten Artefakten** stammen, sonst schlägt die On-Chain-Registrierung fehl und scheitert sicher. Compliance ist ein **Boolean pro Adresse**, geschrieben von demjenigen, den dein KYC-Prozess autorisiert. Und dieselbe Bereitstellung ist von einer Solana-Wallet aus über einen **synthetischen Sender** erreichbar, der on-chain abgeleitet wird — keine Bridge, kein Wrapped Token, keine zweite Allowlist.

## Voraussetzungen

Alle Referenz-Tools liegen in `scripts/` (führe `npm install` dort zuerst aus). Jeder Befehl löst Chain-Fakten aus dem Registry statt sie fest einzubrennen, daher ist das Umstellen auf eine andere Rome-Chain eine Umgebungsänderung, keine Codeänderung:

```bash
export PRIVATE_KEY=…       # finanzierter Schlüssel des Deployers/Emittenten auf der Ziel-Chain
export CHAIN_ID=200010     # wählt die Chain aus
export REGISTRY_ROOT=…     # ein Checkout der Rome-Chain-Registry
# Solana-Lane-Flows benötigen außerdem:
export SOLANA_KEYPAIR=…    # Pfad zu einer finanzierten Solana-Keypair-JSON (dem Fee Payer)
```

Gas auf Rome-Chains ist in USDC denominiert; die komplette Deploy-Sequenz kostet in der Größenordnung von 10–15 nativen Einheiten auf dem Devnet. Schlüssel kommen nur aus der Umgebung — niemals aus dem Repo. Baue die Verträge einmal vor dem Deploy, denn Artefakte **müssen** aus `rome-contracts/out` (siehe Schritt 3):

```bash
cd contracts       && forge build --via-ir     # die vendorte Arc-Suite
cd rome-contracts  && forge build --via-ir     # ArcTokenFactoryV2 + der kanonische Proxy
```

***

## Schritt 1 — Verstehe, was ein berechtigtes Token *ist* hier

Ein berechtigtes Bloom-Token ist nicht ein Vertrag. Es sind **drei bereitgestellte Verträge**, plus vier gemeinsame Verträge, die Ops einmal pro Chain bereitstellt und die jeder Emittent wiederverwendet.

**Die drei pro-Token-Verträge** — ein Asset besitzt genau diese:

| Vertrag                      | Quelle                                                      | Was es ist                                                                                                                           |
| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `ArcTokenProxy`              | `contracts/src/proxy/ArcTokenProxy.sol`                     | Das Token selbst — ein UUPS-Proxy, der auf das gemeinsame `ArcToken` Implementation verweist. Salden, Inhaber und Rollen leben hier. |
| `WhitelistRestrictions`      | `contracts/src/restrictions/WhitelistRestrictions.sol`      | Das Allowlist-Modul. Ein Boolean pro Adresse; wird bei jeder Übertragung durchgesetzt, solange das Asset gegated ist.                |
| `YieldBlacklistRestrictions` | `contracts/src/restrictions/YieldBlacklistRestrictions.sol` | Das Yield-Modul. Schließt Adressen von Yield-Ausschüttungen aus, ohne ihren Bestand anzutasten.                                      |

**Die vier gemeinsamen, einmal-pro-Chain-Verträge** (bereitgestellt von `deploy-infra.ts`):

| Vertrag                     | Rolle                                                                                  |
| --------------------------- | -------------------------------------------------------------------------------------- |
| `RestrictionsRouter`        | Registry für Modultypen — Chain-Verdrahtung, einmal von Ops gesetzt.                   |
| `ArcTokenFactoryV2`         | Registrierung + der kanonische Proxy-Codehash (Schritte 2–3).                          |
| `ArcToken` (Implementation) | Die gemeinsame, wiederverwendbare Asset-Logik, auf die jedes Token per Proxy zugreift. |
| `ArcTokenPurchase`          | Ein gemeinsamer Marktplatz für jedes Token und jeden Emittenten (Schritt 4).           |
| wUSDC                       | Die Cash-Seite — die Yield-Währung und die Kaufwährung beim Verkauf.                   |

Transferbeschränkung und Yield-Beschränkung sind **bewusst getrennte Module**: ein Inhaber kann vom Einkommen ausgeschlossen werden, während er das Asset behält; genau so funktionieren Sanktionen und Gerichtsentscheidungen in der Praxis.

Diese Tatsache „drei Verträge“ ist für die Verifikation entscheidend. `verify-deployments.ts` und `register-asset.ts` erwarten beide, dass sich ein Token auf drei bereitgestellte Verträge auflöst — eine manuelle Verifikation eines Tokens übersah einmal sieben davon über eine Chain hinweg (jedes Yield-Modul und drei Whitelist-Module).

Der Lebenszyklus hat drei Phasen und eine Einbahnstraße:

```
draft     Proxy live, kein Modul verknüpft — nichts blockiert eine Übertragung
  ↓
ungated   Modul verknüpft, transfersAllowed = true   (WhitelistRestrictions.initialize setzt das)
  ↓  Angebot prägen, die Allowlist aufbauen               [umkehrbar]
gated     setTransfersAllowed(false)                [EINBAHNSTRASSE — der Vorrat wird endgültig]
```

Das Gating ist für den Vorrat eine Einbahnstraße, weil Mint und Burn `address(0)` als Gegenpartei verwenden und `address(0)` niemals auf die Allowlist gesetzt werden können — sobald also gegated ist, kann nie wieder neuer Vorrat gemintet werden. Das ist die Garantie, die ein berechtigtes Asset seinen Inhabern gibt.

***

## Schritt 2 — Bereitstellung stückweise, niemals als eine Transaktion

Arc liefert einen Ein-Klick- `createToken` aus, der den Proxy und beide Module bereitstellt und in einem einzigen Aufruf verdrahtet. **Auf Rome kannst du ihn nicht verwenden.** Diese Transaktion braucht **77 Solana-Account-Locks**, und Rome hat ein Limit von **62 Locks**. Sie passt niemals hinein — weder mit Tuning noch mit einem Optimizer-Pass.

Also ist das Deploy **stückweise**: jeder Vertrag in seiner eigenen Transaktion, und das Token erhält den Status eines Factory-Gegenstücks über `ArcTokenFactoryV2.registerToken` statt über das monolithische `createToken`. Das ist der Create-Wizard, und genau das macht die Issuer-Konsole Schritt für Schritt:

![der Create-Wizard](/files/c7a17049183d67558e447572b09c76f83c024a19)

Die Referenzimplementierung ist `arc/plan/create.ts` (`createSequence()`), das die App als sechs Karten rendert (`bloom/lib/createCards.ts`) über **acht Transaktionen**, genau in der Reihenfolge, die die Verträge erzwingen:

| # | Transaktion                             | Vertragsaufruf                                                                  |
| - | --------------------------------------- | ------------------------------------------------------------------------------- |
| 1 | Das Asset bereitstellen                 | `new ArcTokenProxy(impl, initData)` wobei `initData = ArcToken.initialize(...)` |
| 2 | Das Allowlist-Modul bereitstellen       | `new WhitelistRestrictions()`                                                   |
| 3 | Dich als seinen Administrator festlegen | `whitelist.initialize(issuer)`                                                  |
| 4 | Es bei Übertragungen erzwingen          | `token.setRestrictionModule(TRANSFER, whitelist)`                               |
| 5 | Das Yield-Modul bereitstellen           | `new YieldBlacklistRestrictions()`                                              |
| 6 | Dich als seinen Administrator festlegen | `yieldBlacklist.initialize(issuer)`                                             |
| 7 | Es beim Yield erzwingen                 | `token.setRestrictionModule(YIELD, yieldBlacklist)`                             |
| 8 | Bei der Factory registrieren            | `factoryV2.registerToken(token, impl)`                                          |

Die Skriptform ist ein Befehl:

```bash
# einmal pro Chain (Ops, nicht pro Token) — stellt die vier gemeinsamen Verträge bereit
npx tsx deploy-infra.ts

# pro Token — der Wizard, als Skript wiedergegeben
NAME="Mineral Vault I" SYMBOL=ARCV SUPPLY=1000000 DECIMALS=6 npx tsx create-token.ts
```

Der Emittent (`PRIVATE_KEY`) signiert jede Transaktion und hält am Ende die Rollen des Tokens (`ArcToken.initialize` gewährt an `msg.sender` — siehe Schritt 7). Die Registrierung (Tx 8) schaltet den `enableToken` des Marktplatzes und jedes factoryvermittelte Upgrade frei.

> **„Optimiere“ das nicht wieder zu einem Monolithen.** Das Bündeln dieser Aufrufe in einer Transaktion, um Round-Trips zu sparen, bringt genau den 62-Locks-Überlauf zurück, den der stückweise Pfad vermeiden soll. Dasselbe Limit ist der Grund, warum die Yield-Ausschüttung pro Transaktion nur einen Inhaber durchläuft (Schritt 8). Wenn ein Aufruf fehlschlägt mit `Zu viele Konten: N > 62`, greift eine Transaktion auf zu viele Konten zu — aufteilen, nicht tunen.

***

## Schritt 3 — Proxys nur aus den herkunftsverriegelten Artefakten bereitstellen

`registerToken` ist die Sicherheitsgrenze, die stückweise Deploys sicher macht. Sie akzeptiert nur ein Token, dessen **Runtime-Codehash mit dem kanonischen übereinstimmt `ArcTokenProxy`** zur Deploy-Zeit in die Factory eingebrannt wurde:

```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();
    // …die Implementation muss factory-whitelisted sein, und msg.sender muss
    //    die ADMIN_ROLE des Tokens halten, und das Token darf noch nicht registriert sein.
}
```

Weil dieser Codehash ein **immutable**ist, schlägt ein Proxy, der aus einem anderen Artefakt gebaut wurde, sicher fehl mit `ProxyCodehashUnknown`. Daraus folgen zwei Regeln, und beide werden durch CI und die harten Regeln in `CLAUDE.md`:

1. **Token-Proxys MÜSSEN aus `rome-contracts/out`** bereitgestellt werden — niemals aus `contracts/out`, niemals aus einem anderen Build. Das ist die Kompilierung, auf die der kanonische Codehash der Factory fest verdrahtet ist.
2. **`bytecode_hash = "none"` und `cbor_metadata = false`** in `rome-contracts/foundry.toml` sind entscheidend. Mit aktivierten Metadaten trägt der `ArcTokenProxy` Runtime-Code *eingebettet* in der Factory (`type().runtimeCode`) einen anderen CBOR/IPFS-String als das eigenständige `out/` Artefakt, aus dem der Wizard bereitstellt — also schlagen On-Chain `registerToken` Reverts zu, während **jeder Test aus Foundry mit Source-Code besteht** (Foundry bettet beide Kopien ein, daher kann es die Abweichung nicht sehen). `rome-contracts/test/ArtifactProvenance.t.sol` verankert diese Eigenschaft; der gefundene Lauf war es, der sie ursprünglich aufdeckte.

> **Eine grüne in-Source-Test-Suite beweist keine Herkunft.** Bevor du ein Image auslieferst und nach JEGLICHER Änderung der Compiler-Einstellungen, beweise die Eigenschaft gegen die *bereitgestellte* Factory (nur lesend, keine Schlüssel):
>
> ```bash
> cd scripts && CHAIN_ID=<id> npx tsx verify-artifact-provenance.ts
> ```
>
> Es hasht dein lokales `ArcTokenProxy` Artefakt und prüft, dass dieser Hash wörtlich im Bytecode der bereitgestellten Factory erscheint.

Wenn sich Compiler-Einstellungen ändern müssen, **rotiere die Factory** — deploye eine frische Factory, deren Immutable den neuen Codehash fest verdrahtet, und verdrahte neu — statt den Check von Hand zu patchen:

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

Die Rotation behält die alte Adresse im Receipt; Tokens, die gegen die alte Factory registriert wurden, müssen sich gegen die neue neu registrieren.

***

## Schritt 4 — Einen Verkauf über den gemeinsamen Marktplatz öffnen

Verkaufen ist ein eigener Akt, getrennt vom Erstellen, und läuft über den einen gemeinsamen `ArcTokenPurchase` Marktplatz. Bevor ein Verkauf geöffnet wird, müssen drei Dinge wahr sein, und `ArcTokenPurchase.enableToken` prüft jedes davon on-chain:

```solidity
// contracts/src/ArcTokenPurchase.sol
function enableToken(address _tokenContract, uint256 _numberOfTokens, uint256 _tokenPrice)
    external onlyTokenAdmin(_tokenContract)                       // (a) du hältst die ADMIN_ROLE des Tokens
{
    // (b) das Token muss der Factory bekannt sein — das ist es, was registerToken (Schritt 2) gekauft hat:
    if (ArcTokenFactory(ps.tokenFactory).getTokenImplementation(_tokenContract) == address(0))
        revert TokenNotCreatedByFactory();
    // (c) der Marktplatz muss den Verkaufsbestand bereits halten:
    if (ArcToken(_tokenContract).balanceOf(address(this)) < _numberOfTokens)
        revert ContractMissingRequiredTokens();
    // …Preis und Menge müssen positiv sein.
}
```

Es gibt eine vierte, implizite Anforderung, die ein gegated Asset erzwingt: weil **jede Übertragung das Allowlist-Gate durchläuft**, kann das Verschieben von Bestand zum Marktplatz nur funktionieren, wenn die Marktplatz-Adresse selbst auf deinem Token whitelisted ist. Der vollständige Ablauf zum Öffnen eines Verkaufs ist also:

1. **Den Marktplatz whitelisten** auf dem Whitelist-Modul deines Tokens (`batchAddToWhitelist([storefront])` — siehe Schritt 5). Ohne dies schlägt der Inventartransfer in (2) fehl mit `TransferRestricted()` sobald gegated.
2. **Inventar übertragen** an die Marktplatzadresse (aus dem `arcTokenPurchase`).
3. **`enableToken(token, amount, price)`** — Käufer zahlen nun in wUSDC und erhalten das RWA.

Die Aufteilung der Auszahlungen ist auf einem *gemeinsamen* Marktplatz wissenswert, und sie ist nicht symmetrisch (`arc/roles.ts` ist die Autorität):

| Aktion                                                     | Berechtigung                          | Gehalten von                                     |
| ---------------------------------------------------------- | ------------------------------------- | ------------------------------------------------ |
| `enableToken` / `disableToken` / `withdrawUnsoldArcTokens` | die `ADMIN_ROLE` (`onlyTokenAdmin`)   | dir, dem Emittenten                              |
| `withdrawPurchaseTokens` (die wUSDC-Erlöse)                | des Marktplatzes `DEFAULT_ADMIN_ROLE` | wer auch immer den Marktplatz bereitgestellt hat |

Auf einer selbst gehosteten Chain bist du beides. Auf einem gemeinsamen Marktplatz ist der Erlöspool die Gesamteinnahme aller Emittenten, daher ist seine Auszahlung eine Plattform-Entscheidung — entwerfe deine Abwicklung darum herum, statt von alleiniger Kontrolle auszugehen.

***

## Schritt 5 — Binde dein eigenes KYC / deine eigene Compliance ein

Das ist der Abschnitt, den ein Kunde vor seiner Zustimmung liest, also ist er Code, nicht Prosa. **Compliance wird vom Token-Vertrag selbst durchgesetzt** — nicht durch einen Off-Chain-Check, einen Sequencer-Filter oder eine Venue-Policy. Die Regeln reisen mit dem Token auf jedem Ausführungsweg, auf beiden Wallet-Lanes. Was die Chain aufzeichnet, ist das **Ergebnis** deiner KYC-Entscheidung, als ein einzelner Boolean:

```solidity
// contracts/src/restrictions/WhitelistRestrictions.sol — der On-Chain-Status, vollständig
struct WhitelistStorage {
    mapping(address => bool) isWhitelisted;   // ← ein Boolean pro Adresse. Niemals PII.
    bool transfersAllowed;                    // false = gegated; nur whitelisted Adressen dürfen übertragen
    EnumerableSet.AddressSet whitelistedAddresses;
}
```

### Die Schnittstelle, in die dein Prozess schreibt

Die Approve/Deny-Entscheidung deines KYC-Anbieters wird exakt zu einem dieser Aufrufe:

```solidity
interface IWhitelistRestrictions {
    function addToWhitelist(address account) external;                  // nurRole(MANAGER_ROLE)
    function batchAddToWhitelist(address[] calldata accounts) external; // nurRole(MANAGER_ROLE)
    function removeFromWhitelist(address account) external;             // nurRole(MANAGER_ROLE)
    function isWhitelisted(address account) external view returns (bool);
    function setTransfersAllowed(bool allowed) external;                // nurRole(ADMIN_ROLE)
}
```

### Wer schreiben darf

> **Die Berechtigung, die ein Approval braucht, ist `MANAGER_ROLE` auf dem&#x20;*****Modul*****&#x20;— nicht `ADMIN_ROLE` auf dem Token.** `addToWhitelist` / `batchAddToWhitelist` / `removeFromWhitelist` sind alle `nurRole(MANAGER_ROLE)`, und diese Rolle lebt auf der `WhitelistRestrictions` Instanz. Die Authentifizierung `ADMIN_ROLE` am Token und dann in das Modul zu schreiben, ist ein echter Fehler, den diese Codebasis bereits teuer bezahlt hat. Biete nicht `WHITELIST_ADMIN_ROLE` als Lösung für eine Verweigerung an — es wird bei initialize gewährt und **nirgendwo in der Suite geprüft** (`arc/roles.ts` behauptet, dass es die einzige Rolle ist, die nichts absichert).

`WhitelistRestrictions.initialize(issuer)` (Transaktion 3 des Wizards) gewährt dem Issuer `DEFAULT_ADMIN_ROLE`, `ADMIN_ROLE`, `MANAGER_ROLE`, `WHITELIST_ADMIN_ROLE`, und `UPGRADER_ROLE` auf diesem Modul. Also hält der Issuer `MANAGER_ROLE` von Haus aus und kann sie auch delegieren: weil der Issuer `DEFAULT_ADMIN_ROLE`, eine `grantRole(MANAGER_ROLE, serviceAccount)` übergibt den Schreibvorgang an einen Backend-Signer.

### Eine Entscheidung wird zu einem On-Chain-Schreibvorgang

Richte den Webhook deines Anbieters auf einen Signer aus, der `MANAGER_ROLE`, und fasse bei einer Genehmigung die Adressen in einem Batch zusammen:

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

// `signer` muss MANAGER_ROLE auf dem Whitelist-Modul DIESES Tokens halten.
const wallet = createWalletClient({ account, transport: http(chain.rpcUrl) });

// der Anbieter sagte für diese Adressen „genehmigt“:
await wallet.writeContract({
  address: whitelistModule,                 // aus dem Token über getRestrictionModule aufgelöst
  abi: whitelistAbi,
  functionName: 'batchAddToWhitelist',
  args: [approvedAddresses],
});
```

Auflösen `whitelistModule` aus dem Token selbst (`ArcToken.getRestrictionModule(TRANSFER_RESTRICTION_TYPE)`), niemals aus einer gecachten Projektion — ein Schreibvorgang auf das falsche Modul zu richten ist genau so, wie „approve on any asset“ einst in die Allowlist eines einzelnen Tokens schrieb.

### Die wallet-signierte Variante (Self-Service-Demo)

Für ein erlaubnisloses Test-Deployment, bei dem Besucher sich selbst zulassen, gewähre `MANAGER_ROLE` an einen `TestApprover` (`rome-contracts/src/TestApprover.sol`) dessen `Modul` ist **immutable**. Dann `approve(address)` von jedem aus der eigenen Wallet aufrufbar ist, wobei er sein eigenes Gas zahlt — er ruft `addToWhitelist` nur dann auf, wenn die Adresse noch nicht gelistet ist:

```solidity
function approve(address account) external {
    IWhitelistRestrictions target = IWhitelistRestrictions(module);   // nur ein Modul — dauerhaft
    if (!target.isWhitelisted(account)) target.addToWhitelist(account);
    emit Approved(account, msg.sender);
}
```

Das ist das „Sign-in-Gate“ — es zeigt einem Besucher den eigentlichen Mechanismus (eine Genehmigung wird zu einem On-Chain-Allowlist-Eintrag, den der Token dann durchsetzt), ohne irgendetwas zu verwahren. **Gewähre niemals `MANAGER_ROLE` an einen `TestApprover` auf einem Produktions-Token** — dadurch wird die Allowlist dieses Tokens erlaubnislos.

### Wenn das Gate greift, und Anbieter wechseln

Das Gate beschränkt erst, wenn das Asset **gegatet** (`setTransfersAllowed(false)`, `ADMIN_ROLE` auf dem Modul). Solange nicht gegatet, sind Transfers offen — baue zuerst deine Allowlist auf, dann aktiviere das Gate. Sobald gegatet, wird ein Transfer, bei dem eine der beiden Seiten nicht auf der Liste steht, mit dem typisierten Fehler revertiert `TransferRestricted()` (`0xe827105e`).

> **Ein späterer Wechsel des KYC-Anbieters erfordert keine On-Chain-Änderung.** Das Allowlist-Modul, seine Schnittstelle und das von ihm gespeicherte Boolean sind anbieteragnostisch. Um Anbieter zu wechseln, richte eine andere Entscheidungsquelle auf denselben `MANAGER_ROLE` Signer — kein Redeploy, keine Migration, keine Zustandsänderung. Die Chain weiß nie, welcher Anbieter den Aufruf gemacht hat, nur das Ergebnis.

***

## Schritt 6 — Dieselben Assets für Solana-Wallets öffnen

Dieselben drei Verträge sind von einer Solana-Wallet aus erreichbar **ohne Bridge und ohne zweiten Token**. Ein Solana-nativer Nutzer hat nirgendwo einen EVM-Schlüssel; stattdessen ist seine EVM-Identität *synthetisch*, on-chain aus dem tatsächlichen Solana-Signer der Transaktion abgeleitet:

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

Das Rome-EVM-Programm leitet dies direkt aus dem Signer selbst ab (`do_tx_unsigned::derive_sender`), daher kann es nicht gefälscht werden und es existiert kein secp256k1-Privatschlüssel dafür — **die Solana-Signatur ist das Einzige, das diese Adresse jemals steuern kann**. Die Referenzableitung ist `arc/materialise/solana/identity.ts` (`syntheticAddress`), byte-kompatibel mit der On-Chain-Regel:

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

**Was ein Builder tut, um diese Lane zu unterstützen** — beachte, dass *nichts davon eine Vertragsänderung ist*:

1. **Den Solana-Nutzer über seine synthetische Adresse auf die Allowlist setzen.** Für die Compliance-Ebene ist eine synthetische Adresse eine gewöhnliche Adresse: füge den Solana-Pubkey des Nutzers ein, leite die synthetische Adresse ab, und `batchAddToWhitelist([synthetic])`. Eine Allowlist umfasst beide Wallet-Welten.
2. **Richte das Signing-PDA einmal ein, vor ihrer ersten Transaktion.** Das External-Authority-PDA, das die Signatur einer synthetischen Adresse autorisiert, muss zuerst existieren, und es anzulegen ist ein EVM-Lane-Aufruf (`create_pda`) — ein rein Solana-Nutzer kann das nicht selbst tun, daher richtet der Issuer es ein. Das Provisioning ist `external_auth`, nicht lazy.
3. **Über die Lane-Bibliothek einreichen, nicht von Hand.** `submitDoTxUnsigned` (`arc/materialise/solana/submit.ts`) baut ein `DoTxUnsigned` Instruction — ein *unsigniertes* EIP-1559-Payload, autorisiert durch die Solana-Signatur — und behandelt die vier Dinge, die eine von Hand gebaute Transaktion falsch macht:
   * **Account-Ermittlung** über `rome_emulateCallAccounts`, die die Schreibbarkeit jedes Accounts bestimmt. Der `Wert` **müssen** muss an die Ermittlung weitergeleitet werden, sonst markiert ein Value-Transfer das Balance-PDA des Empfängers als nur lesbar und scheitert on-chain.
   * **Compute-Budget** — eine `DoTxUnsigned` benötigt ein **250-KB-Heap-Frame** und ein **1,35 Mio. CU** Limit (Romes EVM sprengt die 32-KB-/200K-Standardwerte); dadurch bleiben etwa 50K Puffer unter Solanas 1,4-Mio.-Obergrenze.
   * **Das Treasure-(Gebühren-)Wallet-PDA**, als schreibbar angehängt — die Ermittlung lässt es weg.
   * **Der v0- + Lookup-Table-Fallback.** Wenn die Accounts eines Aufrufs den 1232-Byte-Rahmen einer Legacy-Transaktion sprengen, reicht der Client den Aufruf erneut als v0-Transaktion über eine frische Address-Lookup-Table ein. (Gemessen passt ein heutiger Storefront-Kauf mit etwa 1.061 von 1232 Bytes in den Legacy-Rahmen; ein Erstbesitzer, dessen Associated Token Account noch angelegt werden muss, kann ihn überschreiten.)

Der Nutzer ist der **Gebührenzahler** und zahlt Lamports (SOL) aus seiner Solana-Wallet; die synthetische Adresse, die er kontrolliert, hält die Assets und niemals SOL. Nach jedem Lane-Send den EVM-Nonce vor dem nächsten abfragen — der Indexer hinkt der Solana-Bestätigung leicht hinterher.

> **Die Lane ist NUR-ATOMAR.** Ein `DoTxUnsigned` ist eine EVM-Transaktion, die von Romes atomarer VM innerhalb einer Solana-Transaktion ausgeführt wird. Die iterative VM (eine EVM-Ausführung, die über mehrere Solana-Transaktionen gestaffelt ist) ist **über diese Lane noch nicht erreichbar**. Ein Aufruf, der atomar nicht hineinpasst, kann diese Lane überhaupt nicht nutzen — die Lösung ist ein kleinerer Aufruf, kein Fallback. Beachte: Der v0+ALT-Fallback ist ein größeres *Umschlag*, keine andere Ausführung; es ist immer noch eine einzige atomare Transaktion.

**Warum das RWA nicht aus dem Perimeter entweichen kann.** Das Asset ist im EVM-Zustand gespeichert — seine Salden sind Storage innerhalb der Accounts des Rome-EVM-Programms. Es gibt **kein SPL-Mint** davon und nichts Solana-Natives, das bewegt werden könnte; die Asset-Bewegungs-Primitiven der Lane arbeiten auf SPL-Token-Accounts, die ein EVM-Storage-Saldo nicht hat. Jede Bewegung, die die Solana-Signatur ausdrücken kann, ist ein Aufruf in den Token-Vertrag, der das Allowlist-Gate durchläuft — die finanzierte Suite stellt fest, dass ein Transfer an eine nicht auf der Allowlist stehende Adresse identisch revertiert, egal ob er von einem EVM-Schlüssel oder einem Solana-Schlüssel signiert ist. Das Cash-Bein (wUSDC) ist die bewusste Ausnahme: Es ist SPL-gestützt und *tut* wird auf das eigene Token-Konto des Nutzers ausgeschüttet.

***

## Schritt 7 — Nach dem Deploy wissen, wer die Autorität hält

`ArcToken.initialize` läuft aus dem Proxy-Konstruktor (Tx 1) und gewährt Rollen an `msg.sender` — den Wizard-Treiber, also den Issuer. Er gewährt **fünf** Rollen explizit:

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

Zwei Folgen, die ein Builder nicht übersehen darf:

* **`MINTER_ROLE`, `BURNER_ROLE`, und `UPGRADER_ROLE` werden bei initialize NICHT gewährt.** Der anfängliche Supply wird innerhalb von `initialize` (ein internes `_mint`, keine Rolle nötig), aber ein *späteres* `mint` oder `burn` erfordert, dass der Issuer sich die Rolle zuerst selbst gewährt. Das können sie, weil sie `DEFAULT_ADMIN_ROLE` (der Admin jeder Rolle — nichts ruft `_setRoleAdmin`, also `DEFAULT_ADMIN_ROLE` verwaltet alle davon). Aber es ist ein bewusster zusätzlicher Schritt, keine allgegenwärtige Befugnis.
* **`UPGRADER_ROLE` wird dem Factory nie gewährt.** Factory-vermittelte Upgrades (`ArcTokenFactoryV2.upgradeToken`) erfordern, dass die **Factory selbst** folgendes hält `UPGRADER_ROLE` auf dem Token — sie sich selbst zu gewähren bewirkt nichts. Diese Gewährung ist eine Einbahnstraße, die die Factory nicht rückgängig machen kann, also ist sie opt-in: Wenn du factory-vermittelte Upgrades willst, `grantRole(UPGRADER_ROLE, factory)` explizit gewähren; andernfalls upgraded der Issuer den Token direkt.

Das vollständige 21-Autoritäten-Modell (11 Rollennamen über sechs Verträge hinweg — derselbe Name ist auf jedem Vertrag eine andere Autorität) ist kodifiziert in `arc/roles.ts`. Frage `rolesRequiredFor(contract, functionName)` statt den eigenen Vertrag einer Rolle zu lesen, denn vier Storefront-Funktionen und beide Factory-Funktionen werden durch eine Autorität abgesichert, die auf einem *anderen* Vertrag.

***

## Schritt 8 — Yield verteilen

Der Token zahlt Yield in wUSDC (festgelegt auf `initialize`, änderbar über `setYieldToken` unter `YIELD_MANAGER_ROLE`). Die Verteilung geht die Holder-Menge durch:

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

Die Mechanik, die für die korrekte Nutzung wichtig ist:

* **Der gesamte `totalAmount` wird vom Aufrufer nur im `startIndex == 0` Fenster** (`safeTransferFrom(msg.sender, this, totalAmount)`). Genehmige diesen Gesamtbetrag dem Token vor dem ersten Fenster; spätere Fenster zahlen aus dem bereits gehaltenen Guthaben aus.
* **`nextIndex` springt zu `0` wenn der Durchlauf abgeschlossen ist.** Schleife `distributeYieldWithLimit(total, nextIndex, …)` bis `nextIndex` zurückkommt `0`.
* **Die Anteile yield-beschränkter Holder bleiben im Token-Vertrag** — sie werden übersprungen, nicht umverteilt.

> **Auf Rome ist die praktische Obergrenze `maxHolders = 1` pro Transaktion** — derselbe 62-Account-Cap aus Schritt 2 — daher wird Yield mit einem Holder pro Transaktion durchlaufen (gemessen etwa 318K Gas pro Holder). Jeder Whitelisted-Holder, ob EVM- oder Solana-nativ, erhält anteilig; das Referenzskript ist `measure-yield-batch.ts` und der Yield-Schritt des finanzierten Smoke-Tests prüft es end-to-end.

```bash
# USAGE.md-Referenzform: einen Holder pro Tx durchlaufen, bis nextIndex auf 0 zurückspringt
distributeYieldWithLimit(total, offset, 1)
```

***

## Schritt 9 — Überprüfen, was du deployt hast

Nichts reicht in Rome Quellcode automatisch irgendwo ein, daher ist die Verifikation ein Schritt, den du selbst ausführst. Nach jedem Deploy — Infrastruktur oder neuer Token — führe den schreibgeschützten, schlüssellosen, idempotenten Verifizierer aus (bereits verifizierte Verträge werden übersprungen, also verifiziert ein erneuter Lauf nach dem Ausgeben eines Tokens nur die neuen):

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

Er erwartet die **drei** Verträge pro Token (Proxy + beide Module) plus die Infrastruktur und meldet, was er nicht verifizieren konnte, statt ein Deploy fehlschlagen zu lassen. Das einmal von Hand zu machen, hat sieben Verträge übersehen.

Die Registrierung des Assets im Rome-Registry behandelt Verifikation als ein **Gate**, nicht als Dekoration (`register-asset.ts`): Ein Token erhält seinen `apps/arc/<chain>.json` Eintrag — mit `standard: arc-permissioned-erc20` und ein `transferRestriction` und benennt sein Allowlist-Gate — nur wenn alle drei seiner Verträge auf Sourcify verifiziert sind. Dieser Eintrag liest seine Fakten aus der Chain (ein per Wizard deployter Token hat keinen festgeschriebenen Beleg) und schreibt nur einen lokalen Registry-Checkout; das Einreichen des PR ist die Aufgabe eines Menschen.

```bash
CHAIN_ID=200010 npx tsx register-asset.ts <tokenAddress>        # verifizieren + den Registry-Eintrag vorbereiten
CHAIN_ID=200010 DRY=1 npx tsx register-asset.ts <tokenAddress>  # nur lesbare Vorschau
```

Für einen End-to-End-Beweis des gesamten Zyklus — erstellen, auf die Allowlist setzen, gaten, auf beiden Lanes verkaufen, Yield verteilen — führe den finanzierten Smoke-Test gegen eine beliebige Chain aus:

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

***

## Schritt 10 — Wissen, was off-chain bleibt

Das Trust-Modell ist absichtlich offen, und ein Builder sollte es seinen eigenen Nutzern genauso darstellen:

* **Das Allowlist-Flag ist deine Off-Chain-Entscheidung.** KYC/AML findet in deinem Prozess statt — deine Anbieter, deine Regeln. Die Chain protokolliert und erzwingt nur das *Ergebnis*, `isWhitelisted(addr)`.
* **Der einzige On-Chain-Zustand ist ein Boolean pro Adresse.** Keine personenbezogenen Daten, keine Dokumente, keine Identitätsbehauptungen, keine On-Chain-Bescheinigungen, kein kryptografischer Beweis, wem eine Adresse gehört. Das ist Arc's Modell, wie es auf Plume verwendet wird; es tauscht die schwereren Abläufe eines On-Chain-Identity-Standards (z. B. ERC-3643) gegen leichtere Mechanik und vom Issuer gehaltenes Vertrauen ein.
* **Wiederherstellung ist eine Befugnis des Issuers, kein Notausgang.** Eine verlorene Wallet — EVM oder Solana — wird wiederhergestellt, indem eine Ersatzadresse auf die Allowlist gesetzt wird und, falls nötig, mint/burn/upgrade im Rahmen deines eigenen rechtlichen Prozesses verwendet werden (Arc hat keine eingebaute Forced-Transfer-Primitivfunktion). Die Schlüsselverwahrung des Issuers ist daher Teil der Compliance-Positionierung.
* **Kein Relayer, keine Verwahrungsfläche.** Nutzer halten ihr eigenes Gas auf beiden Lanes. Ein Fee-Payer-Service würde bedeuten, Schlüssel zu halten — eine Fläche, die ein RWA-Produkt nicht hinzufügen sollte.

***

## Referenz: die Tools

Jedes Skript läuft von `scripts/` nach `npm install`, mit `PRIVATE_KEY`, `CHAIN_ID`, und `REGISTRY_ROOT` exportiert.

| Skript                          | Was es tut                                                                                  |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| `deploy-infra.ts`               | Einmal pro Chain: die vier gemeinsam genutzten Verträge, für wUSDC verdrahtet.              |
| `create-token.ts`               | Der schrittweise Wizard — acht Transaktionen, ein Token.                                    |
| `rotate-factory.ts`             | Nach einer Compiler-/Artefaktänderung eine frische Factory deployen; neu verdrahten.        |
| `verify-artifact-provenance.ts` | Nachweisen, dass dein Proxy-Artefakt von der deployten Factory akzeptiert wird (nur lesen). |
| `verify-deployments.ts`         | Alle drei Verträge pro Token + Infrastruktur auf Sourcify verifizieren (nur lesen).         |
| `register-asset.ts`             | Verifikations-Gate, dann den Compliance-Eintrag im Registry vorbereiten.                    |
| `measure-yield-batch.ts`        | Die Yield-Obergrenze pro Tx auf einer Chain neu messen.                                     |
| `smoke.ts`                      | Die gefundete End-to-End-Suite über beide Lanes hinweg.                                     |
| `sweep-synthetic-native.ts`     | Native Gas von einem synthetischen Konto zurückholen (Test-Hygiene).                        |

Vertiefender Hintergrund findet sich neben diesem Leitfaden: `docs/ARCHITECTURE.md` (das geschichtete App-Modell), `docs/LANES.md` (die zwei Lanes und Verwahrung), `docs/COMPLIANCE.md` (das Trust-Modell), und `docs/USAGE.md` (das Operator-Runbook).


---

# 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/de/apps-auf-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.
