# Vaultfire Forum Registry + XMTP V3 Integration

**Status:** Deployed and live on all 4 production chains (2026-04-27).
**Component:** Discussion Board (`app/sections/AgentHubExtensions.tsx`).
**Contract:** `contracts/VaultfireForumRegistry.sol`.

---

## Overview

The Discussion Board on theloopbreaker.com is wired to **real, end-to-end-encrypted messaging** via XMTP V3 (MLS), with one MLS group per forum category. The mapping from `category → groupId` is stored on-chain in `VaultfireForumRegistry`, deployed independently on each of the four supported chains. First-write-wins: the first user to post in a fresh category creates the XMTP group and registers it on-chain; everyone else syncs to the registered group.

**Why this design:**

- **No central server.** Group IDs live on-chain; messages live in MLS — there is no server you have to trust to host the discussion.
- **Privacy preserved.** XMTP V3 messages are encrypted via MLS; only group members can read them.
- **Censorship-resistant.** A group's group-id is owned by its publisher (per chain) and rotation requires that publisher's key.
- **Honest fallback.** If a user has not yet connected an XMTP wallet, the Discussion Board still accepts local drafts (clearly badged "Local draft") so the UI never lies about what's published.

---

## Deployed Addresses

All four contracts are byte-identical (2,660-byte runtime), Sourcify-"perfect"-verified, and were deployed by `0xfA15Ee28939B222B0448261A22156070f0A7813C` (agent-fa15.vns) on 2026-04-27.

| Chain     | Chain ID | Address                                                                                                          |
| --------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| Base      | 8453     | [`0xB6Ac5aFfF7e5934fc19b16007F26b1866B976240`](https://basescan.org/address/0xB6Ac5aFfF7e5934fc19b16007F26b1866B976240) |
| Avalanche | 43114    | [`0x80f64F6dcdB7aceAfA6762Cd33b14abfF32BcA27`](https://snowtrace.io/address/0x80f64F6dcdB7aceAfA6762Cd33b14abfF32BcA27) |
| Arbitrum  | 42161    | [`0x87404357B9D978E4754c542f54E6724E0cda846e`](https://arbiscan.io/address/0x87404357B9D978E4754c542f54E6724E0cda846e) |
| Polygon   | 137      | [`0x80f64F6dcdB7aceAfA6762Cd33b14abfF32BcA27`](https://polygonscan.com/address/0x80f64F6dcdB7aceAfA6762Cd33b14abfF32BcA27) |

These addresses are exposed to the app at `app/lib/contracts.ts → FORUM_REGISTRY`.

---

## Smart Contract: `VaultfireForumRegistry`

Source: `contracts/VaultfireForumRegistry.sol` · Tests: `test/VaultfireForumRegistry.test.js` (17 passing).

### State

- `mapping(string => Group) groups` — keyed by category name (e.g. `"general"`).
- `string[] categories` — enumerable list of registered category names.

```solidity
struct Group {
    bytes groupId;     // raw XMTP V3 MLS group identifier (opaque bytes)
    address publisher; // first wallet to register this category
    uint64 timestamp;  // block.timestamp at registration
}
```

### Functions

| Selector     | Signature                              | Notes                                                                      |
| ------------ | -------------------------------------- | -------------------------------------------------------------------------- |
| `0x98f9cdb3` | `registerGroup(string,bytes)`          | First-write-wins. Reverts if the category is already claimed.              |
| `0xabef281e` | `getGroup(string)`                     | Returns `(bytes groupId, address publisher, uint64 timestamp)`.            |
| `0xc822d7f0` | `isRegistered(string)`                 | Returns `bool`.                                                            |
| `0xf57617d0` | `listCategories()`                     | Returns `string[]` of all registered categories.                           |
| `0x2be32dbb` | `categoryCount()`                      | Returns `uint256`.                                                         |
| —            | `rotateGroup(string,bytes)`            | Restricted to the original publisher; replaces the groupId for a category. |

Selectors were independently verified against the compiled artifact and against canonical `keccak256(signature)[:4]`.

### Invariants

1. **First-write-wins.** Once `registerGroup` succeeds for a category, it cannot be overwritten by any other wallet.
2. **Publisher-only rotation.** Only the registering wallet can call `rotateGroup` for a given category.
3. **Independent per chain.** There is no cross-chain bridging — each chain holds its own registry, and the app reads from whichever chain the user is currently connected to.

---

## Client Wiring: `app/lib/forum-xmtp.ts`

A self-contained TypeScript module (~385 lines) that:

1. **Wraps the XMTP V3 client** (`@xmtp/browser-sdk@^7`). The signer is built from `window.ethereum.personal_sign` — no private keys ever touch this module.
2. **Encodes/decodes registry RPC** (raw `eth_call` / `eth_sendTransaction` payloads — no extra ABI bundle needed).
3. **Defines the message envelope schema** sent inside MLS groups:

   ```ts
   type Envelope =
     | { v: 1; t: 'post';   id: string; author: string; title: string; body: string; ts: number }
     | { v: 1; t: 'reply';  id: string; parent: string; author: string; body: string; ts: number }
     | { v: 1; t: 'upvote'; id: string; voter: string; ts: number }
     | { v: 1; t: 'unvote'; id: string; voter: string; ts: number };
   ```

4. **Handles first-to-register UX**: when a user posts in a category whose registry slot is empty, the client (a) creates a fresh XMTP MLS group, (b) sends the post into it, and (c) submits a single `registerGroup` transaction on the user's currently connected chain.
5. **Migrates legacy localStorage drafts** so existing on-device posts surface in the new UI as "Local draft".

### Live verification

- **Encoder tests:** `scripts/test-forum-xmtp-encode.js` — 7 / 7 pass against ethers' canonical encoding.
- **Live read tests:** `scripts/test-forum-xmtp-live.js` — 20 / 20 reads succeed across all 4 chains × 5 categories (all return `null`, matching the expected pre-launch empty state).

---

## UI Wiring: `DiscussionBoardTab`

In `app/sections/AgentHubExtensions.tsx`, the Discussion Board now exposes:

- **XMTP connection bar** at the top of the board with:
  - status pill (`disconnected` / `connecting` / `connected` / `error`),
  - "Connect XMTP" button (signs once via the connected wallet, persists across reloads),
  - claimed-groups counter (`claimed: X / 5`).
- **Per-post "Local draft" badge** on any post that has not yet been published into XMTP (e.g. drafts from before the user connected, or pending publishes).
- **First-to-register UX** — the first publisher in an empty category sees a one-time "Claim this category" confirmation explaining that they are creating the group and writing the registry entry.
- **Graceful fallback** — if XMTP is unavailable or the user declines to connect, the board still works with localStorage; nothing is silently dropped.

---

## Deployment & Verification Procedure (for posterity)

> ⚠️ **Never commit private keys.** All deploys use `PRIVATE_KEY` injected only into the shell environment, then `unset` immediately after.

1. **Install + compile**

   ```bash
   npm install
   npx hardhat compile
   ```

2. **Test (must show 17 passing)**

   ```bash
   npx hardhat test test/VaultfireForumRegistry.test.js
   ```

3. **Deploy to all 4 chains**

   ```bash
   export PRIVATE_KEY=0x...        # deployer key, never written to disk
   for net in base avalanche arbitrum polygon; do
     npx hardhat run scripts/deploy-forum-registry.js --network "$net"
   done
   unset PRIVATE_KEY                # SCRUB
   ```

4. **Verify on Sourcify (no API key needed)**

   ```bash
   node scripts/verify-sourcify.js
   ```

   Expected: `status: "perfect"` on all 4 chains. Sourcify metadata is automatically picked up by Basescan, Snowtrace, Arbiscan, and Polygonscan.

5. **Triple-check all chains**

   ```bash
   node scripts/triple-check-forum-registry.js
   ```

   This script confirms (a) bytecode is identical across chains, (b) ABI calls work via `eth_call`, and (c) the initial state is clean (`categoryCount() == 0`, `isRegistered("general") == false`, etc.) on every chain.

6. **Wire frontend**

   `app/lib/contracts.ts` → `FORUM_REGISTRY` map (mirror in `app/app/lib/contracts.ts`).
   `app/lib/forum-xmtp.ts` (mirror in `app/app/lib/forum-xmtp.ts`).
   `app/sections/AgentHubExtensions.tsx` → `DiscussionBoardTab` (mirror in `app/app/sections/`).

---

## Network-Specific Notes

- **Polygon RPC:** `polygon-rpc.com` returns 401 from this VPS. Hardhat is configured to use [`polygon-bor-rpc.publicnode.com`](https://polygon-bor-rpc.publicnode.com) instead.
- **Hardhat verify:** standard `hardhat-etherscan` verification requires per-chain API keys we don't carry; we rely on Sourcify, which is then mirrored automatically by all four explorers.
- **Avalanche EVM:** Cancun EVM is supported on Avalanche post-Etna (Dec 16 2024), so no per-chain Solidity compiler flag changes are needed.

---

## Security Posture

| Concern                                  | Mitigation                                                                                                                |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Replay across chains                     | Each chain holds an independent registry. Group IDs are opaque XMTP MLS bytes and are not portable between chains by design. |
| Squatting on category names              | First-write-wins is the entire model — usernames in the registry are FCFS by category, not by individual handle.          |
| Loss of publisher key                    | `rotateGroup` is publisher-restricted; if a publisher loses their key, their category cannot be migrated. By design.      |
| Private-message leakage                  | XMTP V3 uses MLS — only group members ever see plaintext. The on-chain registry stores only the group ID (bytes), no content. |
| Frontend impersonation                   | All mutations require an EOA signature via the user's wallet. The DiscussionBoardTab never holds key material.            |
| Frontend lying about publish state       | Posts that have not yet been written into XMTP are explicitly badged "Local draft" so the UI never claims a publish that didn't happen. |

---

## References

- VaultfireForumRegistry source: `contracts/VaultfireForumRegistry.sol`
- Test suite: `test/VaultfireForumRegistry.test.js`
- Deploy script: `scripts/deploy-forum-registry.js`
- Sourcify verification: `scripts/verify-sourcify.js`
- Triple-check script: `scripts/triple-check-forum-registry.js`
- Encoder tests: `scripts/test-forum-xmtp-encode.js`
- Live RPC tests: `scripts/test-forum-xmtp-live.js`
- Client wrapper: `app/lib/forum-xmtp.ts`
- UI wiring: `app/sections/AgentHubExtensions.tsx → DiscussionBoardTab`
- XMTP V3 SDK: [`@xmtp/browser-sdk@^7`](https://github.com/xmtp/xmtp-js-content-types)
- Sourcify: [https://sourcify.dev](https://sourcify.dev)
