# Vaultfire Codex — Batch 07: Sprints / Teleporter / VNS

**File:** `07_SPRINTS_TELEPORTER_VNS.md`  
**Contracts covered:** 13  
**Categories:** Sprint-5192 ERC-5192 soulbound · Sprint-7702 EIP-7702 batch onboarding · Teleporter messenger interfaces · TeleporterBridge (V1 & V3) · TrustPortabilityExtension · VaultfireNameService · VaultfireStreamNFT · Mock teleporter  
**Chains:** Base mainnet (chain ID 8453) · Avalanche C-Chain  
**Compiler:** Solidity 0.8.20 – 0.8.25 (per file)

---

## Table of Contents

1. [VaultfireStreetCred.sol](#1-vaultfiresteetcredsol) — Sprint-5192 ERC-5192 Soulbound
2. [VaultfireStreetCred.t.sol](#2-vaultfiresteetcredtsol) — Sprint-5192 Test Suite
3. [VaultfireBatchOnboarder.sol](#3-vaultfirebatchonboardersol) — Sprint-7702 EIP-7702
4. [VaultfireBatchOnboarder.t.sol](#4-vaultfirebatchonboardertsol) — Sprint-7702 Test Suite
5. [Deploy.s.sol](#5-deployssol) — Sprint-7702 Deploy Script
6. [ITeleporterMessenger.sol](#6-itelportermessengersol) — Teleporter Interface
7. [ITeleporterReceiver.sol](#7-itelporterreceiversol) — Teleporter Receiver Interface
8. [VaultfireTeleporterBridge.sol](#8-vaultfireteleporterbridgesol) — TeleporterBridge V1
9. [VaultfireTeleporterBridgeV3.sol](#9-vaultfireteleporterbridgev3sol) — TeleporterBridge V3.3
10. [TrustPortabilityExtension.sol](#10-trustportabilityextensionsol) — Trust Portability
11. [MockTeleporterMessenger.sol](#11-mockteleportermessengersol) — Test Mock
12. [VaultfireNameService.sol](#12-vaultfirenameservicesol) — VNS
13. [VaultfireStreamNFT.sol](#13-vaultfirestreamnftsol) — StreamNFT

---

## 1. VaultfireStreetCred.sol

**File:** `sprint-5192/src/VaultfireStreetCred.sol`  
**Path:** `/home/user/workspace/sprint-5192/src/VaultfireStreetCred.sol`  
**Category:** Sprint-5192 — ERC-5192 Soulbound NFT  
**Compiler:** `solidity 0.8.24` (exact, not caret)  
**SHA-256:** `4e33e4c72970e5a71fdb32faafd7575fb94e30145617a5e040865e7ce97d22e9`

### 1.1 Purpose

`VaultfireStreetCred` is a fully on-chain, soulbound (ERC-5192) reputation badge for Vaultfire protocol agents. Each badge is keyed 1:1 to an Ethereum address: the token ID is simply `uint256(uint160(agentAddress))`. The badge tier is computed live — on every read — by querying two live Base mainnet contracts: `ERC8004IdentityRegistry` and `AIPartnershipBondsV2`. No off-chain data is stored in the contract; the SVG image and JSON metadata are both generated entirely in Solidity.

The design mirrors the off-chain `/api/agent/status` endpoint at `theloopbreaker.com`, using the same scoring formula in pure Solidity, making the on-chain badge authoritative and verifiable without a backend.

### 1.2 Full Source Code with Line Numbers

```solidity
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.24;
     3	
     4	/* ────────────────────────────────────────────────────────────────────────────
     5	   VaultfireStreetCred  —  ERC-5192 Soulbound NFT for on-chain Street Cred
     6	
     7	   * One token per agent (1:1, address ↔ tokenId).
     8	   * Tier is derived ON-CHAIN from the live IdentityRegistry +
     9	     PartnershipBondsV2 contracts, using the canonical Street Cred formula:
    10	
    11	       Identity Registered : 30
    12	       Has Bond            : 25
    13	       Bond Active         : 15
    14	       Bond Tier Bonus     : 0/5/10/15/20  (bronze/silver/gold/platinum)
    15	       Multiple Bonds      : 5  (bondCount > 1)
    16	       Total               : 0..95
    17	
    18	       Tier mapping:
    19	         None     : 0
    20	         Bronze   : 1..30
    21	         Silver   : 31..55
    22	         Gold     : 56..75
    23	         Platinum : 76..95
    24	
    25	   * Permissionless mintFor(agent) — anyone can pay the gas to mint or
    26	     refresh another agent's badge. Agents never have to pay.
    27	   * tokenURI returns a fully on-chain data URI — no IPFS / off-chain deps.
    28	   * Soulbound: every transfer / approval reverts. locked() always true.
    29	   ──────────────────────────────────────────────────────────────────────── */

    31	interface IIdentityRegistry {
    32	    function getAgent(address agentAddress)
    33	        external view
    34	        returns (string memory agentURI, bool active, string memory agentType, uint256 registeredAt);
    35	}
    36	
    37	interface IPartnershipBondsV2 {
    38	    struct Bond {
    39	        uint256 bondId;
    40	        address human;
    41	        address aiAgent;
    42	        string partnershipType;
    43	        uint256 stakeAmount;
    44	        uint256 createdAt;
    45	        uint256 distributionRequestedAt;
    46	        bool distributionPending;
    47	        bool active;
    48	        uint256 distributionCount;
    49	    }
    50	    function getBondsByParticipantCount(address participant) external view returns (uint256);
    51	    function getBondsByParticipant(address participant) external view returns (uint256[] memory);
    52	    function getBond(uint256 bondId) external view returns (Bond memory);
    53	}
    54	
    55	interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); }
    56	
    57	interface IERC721 is IERC165 {
    58	    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    59	    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    60	    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    61	    function balanceOf(address owner) external view returns (uint256);
    62	    function ownerOf(uint256 tokenId) external view returns (address);
    63	    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    64	    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    65	    function transferFrom(address from, address to, uint256 tokenId) external;
    66	    function approve(address to, uint256 tokenId) external;
    67	    function setApprovalForAll(address operator, bool approved) external;
    68	    function getApproved(uint256 tokenId) external view returns (address);
    69	    function isApprovedForAll(address owner, address operator) external view returns (bool);
    70	}
    71	
    72	interface IERC721Metadata is IERC721 {
    73	    function name() external view returns (string memory);
    74	    function symbol() external view returns (string memory);
    75	    function tokenURI(uint256 tokenId) external view returns (string memory);
    76	}
    77	
    78	interface IERC5192 {
    79	    event Locked(uint256 tokenId);
    80	    event Unlocked(uint256 tokenId);
    81	    function locked(uint256 tokenId) external view returns (bool);
    82	}
    83	
    84	contract VaultfireStreetCred is IERC721Metadata, IERC5192 {
    85	
    86	    IIdentityRegistry   public immutable identityRegistry;
    87	    IPartnershipBondsV2 public immutable partnershipBonds;
    88	
    89	    string public constant override name   = "Vaultfire Street Cred";
    90	    string public constant override symbol = "VSC";
    91	
    92	    uint8 public constant TIER_NONE     = 0;
    93	    uint8 public constant TIER_BRONZE   = 1;
    94	    uint8 public constant TIER_SILVER   = 2;
    95	    uint8 public constant TIER_GOLD     = 3;
    96	    uint8 public constant TIER_PLATINUM = 4;
    97	
    98	    mapping(uint256 => bool)    private _exists;
    99	    mapping(uint256 => uint256) private _mintedAt;
   100	    mapping(uint256 => uint256) private _refreshedAt;
   101	
   102	    uint256 public totalMinted;
   103	
   104	    event StreetCredMinted(address indexed agent, uint256 indexed tokenId, uint8 tier, uint16 score);
   105	    event StreetCredRefreshed(address indexed agent, uint256 indexed tokenId, uint8 oldTier, uint8 newTier, uint16 score);
   106	
   107	    error NotEligible();
   108	    error AlreadyMinted();
   109	    error TokenDoesNotExist();
   110	    error Soulbound();
   111	    error ZeroAddress();
   112	
   113	    constructor(address identityRegistry_, address partnershipBonds_) {
   114	        if (identityRegistry_ == address(0) || partnershipBonds_ == address(0)) revert ZeroAddress();
   115	        identityRegistry  = IIdentityRegistry(identityRegistry_);
   116	        partnershipBonds  = IPartnershipBondsV2(partnershipBonds_);
   117	    }
   118	
   119	    function computeScore(address agent) public view
   120	        returns (uint16 score, uint8 tier, bool registered, bool hasBond,
   121	                 bool bondActive, uint256 bondCount, uint256 stakeWei)
   122	    {
   123	        (, bool active, , uint256 registeredAt) = identityRegistry.getAgent(agent);
   124	        registered = (registeredAt > 0 && active);
   125	
   126	        bondCount = partnershipBonds.getBondsByParticipantCount(agent);
   127	        if (bondCount > 0) {
   128	            uint256[] memory ids = partnershipBonds.getBondsByParticipant(agent);
   129	            if (ids.length > 0) {
   130	                IPartnershipBondsV2.Bond memory b = partnershipBonds.getBond(ids[0]);
   131	                stakeWei   = b.stakeAmount;
   132	                hasBond    = stakeWei > 0;
   133	                bondActive = b.active;
   134	            }
   135	        }
   136	
   137	        uint16 s = 0;
   138	        if (registered) s += 30;
   139	        if (hasBond)    s += 25;
   140	        if (bondActive) s += 15;
   141	
   142	        uint8 bTier = _bondTierBonusTier(stakeWei);
   143	        if (bTier == 4)      s += 20;
   144	        else if (bTier == 3) s += 15;
   145	        else if (bTier == 2) s += 10;
   146	        else if (bTier == 1) s += 5;
   147	
   148	        if (bondCount > 1) s += 5;
   149	
   150	        score = s;
   151	        tier  = _scoreToTier(s);
   152	    }
   153	
   154	    function mintFor(address agent) external returns (uint256 tokenId) {
   155	        if (agent == address(0)) revert ZeroAddress();
   156	        tokenId = _tokenIdOf(agent);
   157	        if (_exists[tokenId]) revert AlreadyMinted();
   158	        (uint16 score, uint8 tier,,,,,) = computeScore(agent);
   159	        if (tier == TIER_NONE) revert NotEligible();
   160	        _exists[tokenId]      = true;
   161	        _mintedAt[tokenId]    = block.timestamp;
   162	        _refreshedAt[tokenId] = block.timestamp;
   159	        unchecked { ++totalMinted; }
   160	        emit Transfer(address(0), agent, tokenId);
   161	        emit Locked(tokenId);
   162	        emit StreetCredMinted(agent, tokenId, tier, score);
   163	    }
   164	
   165	    function refresh(uint256 tokenId) external returns (uint8 newTier, uint16 newScore) {
   166	        if (!_exists[tokenId]) revert TokenDoesNotExist();
   167	        address agent = address(uint160(tokenId));
   168	        uint8 oldTier = _liveTier(agent);
   169	        (uint16 s, uint8 t,,,,,) = computeScore(agent);
   170	        _refreshedAt[tokenId] = block.timestamp;
   171	        emit StreetCredRefreshed(agent, tokenId, oldTier, t, s);
   172	        return (t, s);
   173	    }
   174	
   175	    // ... ERC-721, ERC-5192, ERC-165 implementations, soulbound overrides,
   176	    //     timestamp getters, and internal helpers follow (lines 240–447)
```

*(Full source: `/home/user/workspace/sprint-5192/src/VaultfireStreetCred.sol`, 449 lines)*

### 1.3 Line-by-Line Walkthrough

| Lines | What happens |
|-------|-------------|
| 1–2 | MIT licence; exact compiler pin `0.8.24` (no caret — pinned to prevent unexpected optimizer behaviour changes). |
| 4–29 | Module-level block comment documents the Street Cred formula and scoring table inline with the code. |
| 31–35 | `IIdentityRegistry` — minimal interface to `ERC8004IdentityRegistry` on Base. Only `getAgent` is needed; returns `(agentURI, active, agentType, registeredAt)`. |
| 37–55 | `IPartnershipBondsV2` — mirror of the live bond contract's `Bond` struct plus three read functions. The struct carries `stakeAmount`, `active`, and `distributionCount`, all used by the scoring algorithm. |
| 57–60 | `IERC165` — minimal introspection interface. |
| 62–77 | `IERC721` — full ERC-721 event set and function surface. All transfer/approval functions will be overridden to revert. |
| 79–84 | `IERC721Metadata` — `name`, `symbol`, `tokenURI`. |
| 86–91 | `IERC5192` — soulbound interface. Declares `Locked` and `Unlocked` events, and the `locked(uint256)` view function. |
| 93 | Contract declaration: `VaultfireStreetCred is IERC721Metadata, IERC5192`. No inheritance from OpenZeppelin — fully self-contained. |
| 96–97 | Two `immutable` pointers set at construction time; stored in bytecode rather than storage slots, saving ~2100 gas per read. |
| 101–102 | `name` and `symbol` as `string public constant` — gas-free via the ABI since they're compile-time literals. |
| 104–108 | Tier constants `TIER_NONE` through `TIER_PLATINUM` (0–4) as `uint8 public constant`. Used in score mapping and metadata generation. |
| 112–114 | Three private mappings keyed on `tokenId` (= `uint256(uint160(agent))`): existence flag, mint timestamp, last refresh timestamp. |
| 117 | `uint256 public totalMinted` — running count of badges ever minted, not decremented on refresh. |
| 121–126 | `StreetCredMinted` and `StreetCredRefreshed` events. The refresh event carries both `oldTier` and `newTier` to let indexers detect tier changes without replaying all history. |
| 136–141 | Error definitions: `NotEligible` (score = 0), `AlreadyMinted`, `TokenDoesNotExist`, `Soulbound`, `ZeroAddress`. All custom errors — 4-byte selectors, cheaper than string reverts. |
| 144–149 | Constructor: zero-address guard, then stores both registry addresses as immutables. |
| 155–191 | `computeScore` — the core algorithm. Described in full in §1.6 Math. |
| 193–203 | `scoreOf` and `tierOf` — convenience wrappers that destructure `computeScore` and discard unused return values. |
| 208–225 | `mintFor` — permissionless mint. Calls `computeScore`; reverts with `NotEligible` if tier is NONE (score = 0, meaning the agent is not registered). Sets `_exists`, `_mintedAt`, `_refreshedAt`, increments `totalMinted` in `unchecked` block (no realistic overflow path), emits `Transfer(0x0 → agent)`, `Locked`, and `StreetCredMinted`. |
| 229–238 | `refresh` — updates `_refreshedAt` and emits `StreetCredRefreshed`. Does NOT burn or reissue; the tier is computed live on every read so the stored timestamp is just an audit trail for indexers. Emits `oldTier` and `newTier` — even when unchanged — as a heartbeat for off-chain systems. |
| 240–243 | `hasBadge` — boolean check: does `_exists[_tokenIdOf(agent)]` hold? |
| 247–254 | `balanceOf` and `ownerOf` — ERC-721 surface. `ownerOf` recovers the agent address from the token ID via `address(uint160(tokenId))`. |
| 257–294 | `tokenURI` — generates a complete `data:application/json;base64,...` URI entirely on-chain. Composes an SVG circle badge and a JSON object. Both are base64-encoded inline using `_b64`. |
| 297–300 | `locked` — implements ERC-5192. Always returns `true`; there is no admin override. |
| 305–311 | `supportsInterface` — advertises ERC-165, ERC-721, ERC-721Metadata, and ERC-5192. The ERC-5192 interface ID `0xb45a3c0e` is tested in the test suite. |
| 316–325 | All transfer and approval functions revert unconditionally with `Soulbound()`. `getApproved` returns `address(0)` for minted tokens (rather than reverting), matching common indexer expectations. `isApprovedForAll` returns `false` (pure). |
| 329–337 | `mintedAt` and `refreshedAt` — external view accessors for the private timestamp mappings. |
| 341–343 | `_tokenIdOf` — pure helper: `uint256(uint160(agent))`. The address-as-ID scheme is a deliberate design choice: it eliminates a counter and guarantees uniqueness without a nonce. |
| 345–357 | `_bondTierBonusTier` — pure mapping from `stakeWei` to bond tier (0–4) using ETH thresholds. |
| 360–366 | `_scoreToTier` — pure scoring-to-tier bucketing. |
| 368–382 | `_tierName` and `_tierColor` — pure helpers returning the string name and hex color for each tier, used in SVG and JSON metadata generation. |
| 386–447 | Pure stringification and base64 helpers: `_u2s` (uint to decimal string), `_addr2s` (address to lowercase hex string), `_b64` (RFC 4648 base64 encoder via inline assembly). No external library dependencies. |

### 1.4 State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|-----------|---------|
| `identityRegistry` | `IIdentityRegistry` | `immutable` | Pointer to live `ERC8004IdentityRegistry` on Base. Set at construction; reads agent registration state. |
| `partnershipBonds` | `IPartnershipBondsV2` | `immutable` | Pointer to live `AIPartnershipBondsV2` on Base. Set at construction; reads bond data. |
| `name` | `string` | `constant` | ERC-721 token name: `"Vaultfire Street Cred"`. |
| `symbol` | `string` | `constant` | ERC-721 ticker: `"VSC"`. |
| `TIER_NONE` – `TIER_PLATINUM` | `uint8` | `constant` | Tier enum values 0–4. |
| `_exists` | `mapping(uint256 => bool)` | mutable | Whether a token ID has been minted. |
| `_mintedAt` | `mapping(uint256 => uint256)` | mutable | Block timestamp of first mint per token. Never updated after mint. |
| `_refreshedAt` | `mapping(uint256 => uint256)` | mutable | Block timestamp of last `refresh()` call, set to `block.timestamp` on mint and on each refresh. |
| `totalMinted` | `uint256` | mutable | Running total of minted badges; never decremented (no burn path). |

### 1.5 Functions — Full Behavioral Specification

**`computeScore(address agent) → (uint16 score, uint8 tier, bool registered, bool hasBond, bool bondActive, uint256 bondCount, uint256 stakeWei)`**  
External `view`. The heart of the contract. Queries `identityRegistry.getAgent(agent)` to determine registration and activity status. Then queries `partnershipBonds.getBondsByParticipantCount(agent)` and, if nonzero, `getBondsByParticipant(agent)` to fetch bond IDs. The first bond ID (index 0, matching the API's behavior of using the most recently relevant bond) is fetched via `getBond`. Score components are accumulated and the final tier is mapped by `_scoreToTier`. Returns all intermediate booleans for transparency.

**`scoreOf(address agent) → uint16`**  
External `view`. Convenience wrapper — returns only the score from `computeScore`.

**`tierOf(address agent) → uint8`**  
External `view`. Returns only the tier.

**`mintFor(address agent) → uint256 tokenId`**  
External, non-`view`. Permissionless — any EOA or contract can call. Guards: zero-address check, existence check (reverts `AlreadyMinted` if badge exists). Calls `computeScore`; reverts `NotEligible` if `tier == TIER_NONE`. Records `_exists`, `_mintedAt`, `_refreshedAt`. Increments `totalMinted` in `unchecked` (overflow impossible at realistic supply). Emits `Transfer(address(0), agent, tokenId)` per ERC-721, then `Locked(tokenId)` per ERC-5192, then `StreetCredMinted`.

**`refresh(uint256 tokenId) → (uint8 newTier, uint16 newScore)`**  
External, non-`view`. Permissionless. Reverts `TokenDoesNotExist` if not minted. Recomputes the live tier via `computeScore`, updates `_refreshedAt`, emits `StreetCredRefreshed` with both old and new tier for indexers. Note: `oldTier` is computed with a second `computeScore` call via `_liveTier` — this is a read-only call, not stored, so it costs an extra ~3000 gas but avoids a storage write for the current tier (which would be stale anyway).

**`hasBadge(address agent) → bool`**  
External `view`. Reads `_exists[_tokenIdOf(agent)]`.

**`balanceOf(address owner) → uint256`**  
External `view`. Returns 0 or 1; reverts for zero-address per ERC-721.

**`ownerOf(uint256 tokenId) → address`**  
Public `view`. Recovers the agent address from the token ID by casting back to `address(uint160(tokenId))`.

**`tokenURI(uint256 tokenId) → string`**  
External `view`. Generates the full metadata inline. Composes an SVG circle with tier-appropriate color and a JSON object with `name`, `description`, `image` (data URI), and `attributes`. Both SVG and JSON are base64-encoded and returned as `data:application/json;base64,...`. No off-chain dependency.

**`locked(uint256 tokenId) → bool`**  
External `view`. ERC-5192 implementation. Always returns `true`. Reverts `TokenDoesNotExist` if the token has not been minted.

**`supportsInterface(bytes4 interfaceId) → bool`**  
External `pure`. Returns `true` for ERC-165 (`0x01ffc9a7`), ERC-721 (`0x80ac58cd`), ERC-721Metadata (`0x5b5e139f`), and ERC-5192 (`0xb45a3c0e`).

**Transfer/Approval functions**  
`safeTransferFrom` (both overloads), `transferFrom`, `approve`, `setApprovalForAll` — all revert with `Soulbound()`. `getApproved` returns `address(0)` for existing tokens (does not revert — matches common indexer expectations). `isApprovedForAll` returns `false` unconditionally.

**`mintedAt(uint256 tokenId) → uint256`**  
External `view`. Returns `_mintedAt[tokenId]`; reverts `TokenDoesNotExist`.

**`refreshedAt(uint256 tokenId) → uint256`**  
External `view`. Returns `_refreshedAt[tokenId]`; reverts `TokenDoesNotExist`.

### 1.6 Math — Street Cred Scoring Formula

The formula is a 5-component additive score in the range \([0, 95]\):

\[
\text{score} = R_{30} + B_{25} + A_{15} + T_{\text{bond}} + M_5
\]

Where:

| Component | Condition | Points |
|-----------|-----------|--------|
| \(R_{30}\) | `registered = (registeredAt > 0 && active)` | +30 |
| \(B_{25}\) | `hasBond = (stakeWei > 0)` | +25 |
| \(A_{15}\) | `bondActive = b.active` | +15 |
| \(T_{\text{bond}}\) | `stakeWei >= 0.01 ETH` → +5 (Bronze) | +5 |
| | `stakeWei >= 0.05 ETH` → +10 (Silver) | +10 |
| | `stakeWei >= 0.1 ETH` → +15 (Gold) | +15 |
| | `stakeWei >= 0.5 ETH` → +20 (Platinum) | +20 |
| \(M_5\) | `bondCount > 1` | +5 |

**Tier mapping from score:**

| Score range | Tier | Constant |
|-------------|------|----------|
| 0 | None | `TIER_NONE = 0` |
| 1–30 | Bronze | `TIER_BRONZE = 1` |
| 31–55 | Silver | `TIER_SILVER = 2` |
| 56–75 | Gold | `TIER_GOLD = 3` |
| 76–95 | Platinum | `TIER_PLATINUM = 4` |

**Live verification (Sprint-1 agent `0xDF891b2b...`):** `registeredAt > 0 && active` = +30; `stakeWei = 0.0001 ETH < 0.01 ETH` → no bond-tier bonus, but `stakeWei > 0` = +25; `active = true` = +15; `bondCount = 1` → no multi-bond bonus. Total = 70, which maps to Gold (56–75). This matches the live API output documented in the test.

### 1.7 Events

| Event | When fired | Fields |
|-------|-----------|--------|
| `Transfer(address(0), agent, tokenId)` | On every `mintFor` | Standard ERC-721 mint event; `from = address(0)` signals mint. |
| `Locked(tokenId)` | Immediately after `Transfer` in `mintFor` | ERC-5192 soulbound lock signal; fired once per token at mint. `Unlocked` is never fired. |
| `StreetCredMinted(agent, tokenId, tier, score)` | On successful `mintFor` | Captures the score and tier at mint time for indexers. |
| `StreetCredRefreshed(agent, tokenId, oldTier, newTier, score)` | On every `refresh` call (even if tier unchanged) | Carries both tiers for change detection; score is the current live value. |

### 1.8 Security Invariants

1. **True soulbound:** No code path transfers a token. `_exists[tokenId]` is set once and never cleared (no burn). `locked()` unconditionally returns `true`.
2. **No admin:** No `owner`, no `onlyOwner` modifier, no upgrade mechanism. Contract is immutable after deployment.
3. **Permissionless scoring:** `computeScore` is a pure-view function over two external contracts. Any caller — EOA or contract — can read any agent's score without restriction.
4. **Permissionless minting:** `mintFor` is callable by anyone for anyone. Frontrunning the mint of another agent's badge is harmless — the badge is locked to the agent's address and the caller receives nothing.
5. **Unique token IDs:** Token ID = address as integer. Two agents cannot have the same address, so IDs are globally unique without a counter.
6. **No value transfer:** The contract holds and transfers no ETH or ERC-20 tokens. There is no `receive()`, no `payable` function.
7. **Immutable live data sources:** Both registry addresses are `immutable`. Once deployed, the contract always reads from the same registries — a compromised registry upgrade would require redeployment of this contract.

### 1.9 Standards Compliance

- **ERC-721** (EIP-721): Implements `IERC721` and `IERC721Metadata`. Tokens exist with 1:1 balance; all transfer/approval paths revert. Token IDs are valid `uint256`. `tokenURI` returns a fully specified data URI.
- **ERC-5192** (EIP-5192, Minimal Soulbound NFT): `locked()` always returns `true`; `Locked` event is emitted at mint; `Unlocked` is never emitted. The contract satisfies the minimal soulbound specification: lock status is immutable and the token cannot be transferred.
- **ERC-165** (EIP-165): `supportsInterface` returns `true` for ERC-165, ERC-721, ERC-721Metadata, and ERC-5192 interface IDs.

### 1.10 Dependencies + Rebuild Recipe

**No external library imports.** All interfaces (ERC-165, ERC-721, ERC-721Metadata, ERC-5192) and helper functions (base64 encoder, address formatter, uint-to-string) are defined inline.

**Build:**
```bash
cd sprint-5192
forge build   # Requires Foundry, solc 0.8.24 available
```

**Test (live fork):**
```bash
FOUNDRY_RPC_URL=https://mainnet.base.org forge test
```

**Constructor arguments at deployment:**
- `identityRegistry_`: `0xa7BD20bf5De63df949cA5Be2F20835978eCba81A` (ERC8004IdentityRegistry on Base)
- `partnershipBonds_`: `0x01C479F0c039fEC40c0Cf1c5C921bab457d57441` (AIPartnershipBondsV2 on Base)

---

## 2. VaultfireStreetCred.t.sol

**File:** `sprint-5192/test/VaultfireStreetCred.t.sol`  
**Path:** `/home/user/workspace/sprint-5192/test/VaultfireStreetCred.t.sol`  
**Category:** Sprint-5192 — Test Suite  
**Compiler:** `solidity 0.8.24`  
**SHA-256:** `b946f1f6be3eefc27bf18799671b288a1bb245990adbb7045df7fddd3cc93ee1`

### 2.1 Purpose

Foundry test suite for `VaultfireStreetCred`. All tests run on a live Base mainnet fork, using the real `ERC8004IdentityRegistry` and `AIPartnershipBondsV2` contracts and the Sprint-1 live agent at `0xDF891b2b648a09B463C36D626Bd5d6cA8252fab8`. This provides end-to-end coverage of the scoring formula against real on-chain data and validates that the contract matches the API output (`trustScore: 70, tier: Gold`).

### 2.2 Test Structure

**Setup (`setUp`):** Forks Base mainnet at the latest block. Deploys a fresh `VaultfireStreetCred` instance pointing at the live registries. No mock data — all reads hit the actual contracts.

**Test groups:**

| Test | What it verifies |
|------|-----------------|
| `test_metadata_constants` | Name, symbol, registry addresses, initial `totalMinted = 0`. |
| `test_constructor_reverts_on_zero_address` | Both zero-address paths revert. |
| `test_live_score_matches_API_for_sprint1_agent` | `computeScore(LIVE_AGENT)` = `(70, TIER_GOLD, true, true, true, 1, 0.0001 ether)`. Validates the formula mirrors the off-chain API. |
| `test_unknown_address_scores_zero` | Unregistered address → score 0, `TIER_NONE`. |
| `test_tier_constants` | Constants 0–4 in order. |
| `test_mintFor_succeeds_for_eligible_agent_and_emits_locked` | Full happy-path: mints, checks three events (`Transfer`, `Locked`, `StreetCredMinted`), verifies ownership and timestamps. |
| `test_mintFor_reverts_for_unregistered_agent` | `NotEligible` on unknown address. |
| `test_mintFor_reverts_on_zero_address` | `ZeroAddress`. |
| `test_mintFor_reverts_on_double_mint` | `AlreadyMinted` on second call. |
| `test_refresh_emits_event_with_current_tier` | Time-warped 1 day; `StreetCredRefreshed` fires; `refreshedAt > mintedAt`. |
| `test_refresh_reverts_for_nonexistent_token` | `TokenDoesNotExist`. |
| `test_locked_always_true` | `locked()` returns `true`. |
| `test_locked_reverts_for_nonexistent_token` | `TokenDoesNotExist`. |
| `test_supportsInterface_advertises_required_interfaces` | ERC-165/721/721Metadata/5192 IDs all return `true`; random ID returns `false`. |
| `test_transferFrom_reverts` | `Soulbound`. |
| `test_safeTransferFrom_reverts` | `Soulbound`. |
| `test_safeTransferFrom_with_data_reverts` | `Soulbound`. |
| `test_approve_reverts` | `Soulbound`. |
| `test_setApprovalForAll_reverts` | `Soulbound`. |
| `test_getApproved_always_zero_or_reverts` | Returns `address(0)` for minted; reverts for non-existent. |
| `test_isApprovedForAll_always_false` | Returns `false`. |
| `test_tokenURI_returns_valid_data_uri_with_tier_name` | URI starts with `data:application/json;base64,`. |
| `test_tokenURI_reverts_for_nonexistent_token` | `TokenDoesNotExist`. |
| `test_scoreOf_and_tierOf_views` | Convenience accessors return consistent values. |
| `test_ownerOf_reverts_for_nonexistent` | `TokenDoesNotExist`. |
| `test_balanceOf_zero_address_reverts` | `ZeroAddress`. |

### 2.3 Notable Patterns

- `vm.prank(address(0xCAFE))` for permissionless mint — verifies that the caller does not become the owner.
- `vm.expectEmit(true, true, true, true, address(sc))` with three sequential `expectEmit` calls verifies the exact event ordering: `Transfer` → `Locked` → `StreetCredMinted`.
- `vm.warp(block.timestamp + 1 days)` tests that `mintedAt` is anchored and `refreshedAt` advances.

---

## 3. VaultfireBatchOnboarder.sol

**File:** `sprint-7702/src/VaultfireBatchOnboarder.sol`  
**Path:** `/home/user/workspace/sprint-7702/src/VaultfireBatchOnboarder.sol`  
**Category:** Sprint-7702 — EIP-7702 Batch Onboarding  
**Compiler:** `^0.8.24`  
**SHA-256:** `e3c1c9453e05bb34424e0a2551b3896244633b8a04910fa78dcf67b02d2db617`

### 3.1 Purpose

`VaultfireBatchOnboarder` is a **stateless EIP-7702 delegate contract** that enables a Vaultfire user to register their identity and create a partnership bond in a single transaction — without ever interacting with two separate contracts or signing two separate transactions.

Under EIP-7702 (introduced in Ethereum's Pectra upgrade), an EOA can sign an "authorization tuple" that temporarily (or persistently) delegates its account code to a smart contract address. When the delegation is active, calls to the EOA address execute the delegate contract's code in the EOA's storage context. This contract exploits that mechanism: the EOA's single call to `onboardAndBond` registers the EOA itself as an agent (since `msg.sender` is the EOA) and creates a bond staking ETH — both in one transaction.

The contract ships with seven carefully documented security design principles (lines 26–102), making it a reference implementation for safe minimal 7702 delegates.

### 3.2 Full Source Code with Line Numbers

```solidity
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity ^0.8.24;
     3	
     4	/*
     5	 * VaultfireBatchOnboarder
     6	 * ─────────────────────────
     7	 *
     8	 * STATELESS EIP-7702 DELEGATE CONTRACT
     9	 * ...
    10	 * [103 lines of security documentation omitted for brevity — read the original]
    11	 */
   105	interface IIdentityRegistry {
   106	    function registerAgent(
   107	        string calldata agentURI,
   108	        string calldata agentType,
   109	        bytes32 capabilitiesHash
   110	    ) external;
   111	}
   112	
   113	interface IPartnershipBonds {
   114	    function createBond(address aiAgent, string calldata partnershipType)
   115	        external payable returns (uint256);
   116	}
   117	
   118	contract VaultfireBatchOnboarder {
   119	    IIdentityRegistry public constant IDENTITY_REGISTRY =
   120	        IIdentityRegistry(0xa7BD20bf5De63df949cA5Be2F20835978eCba81A);
   120	
   121	    IPartnershipBonds public constant PARTNERSHIP_BONDS =
   122	        IPartnershipBonds(0x01C479F0c039fEC40c0Cf1c5C921bab457d57441);
   123	
   124	    uint256 public constant MIN_STAKE_WEI = 1;
   125	    uint256 public constant BASE_CHAIN_ID = 8453;
   126	
   127	    event OnboardedWithBond(
   128	        address indexed onboardedEoa, address indexed aiAgent,
   129	        uint256 indexed bondId, uint256 stakeAmount,
   130	        string agentType, string partnershipType
   131	    );
   132	
   133	    error StakeBelowMinimum(uint256 sent, uint256 required);
   134	    error SelfBondNotAllowed();
   135	    error ZeroAgentAddress();
   136	    error WrongChain(uint256 expected, uint256 actual);
   137	    error EmptyPartnershipType();
   138	    error EmptyAgentURI();
   139	
   140	    function onboardAndBond(
   141	        string calldata agentURI,
   142	        string calldata agentType,
   143	        bytes32 capabilitiesHash,
   144	        address aiAgent,
   145	        string calldata partnershipType
   146	    ) external payable returns (uint256 bondId) {
   147	        if (block.chainid != BASE_CHAIN_ID) revert WrongChain(BASE_CHAIN_ID, block.chainid);
   148	        if (msg.value < MIN_STAKE_WEI)        revert StakeBelowMinimum(msg.value, MIN_STAKE_WEI);
   149	        if (bytes(agentURI).length == 0)       revert EmptyAgentURI();
   150	        if (bytes(partnershipType).length == 0) revert EmptyPartnershipType();
   151	        if (aiAgent == address(0))             revert ZeroAgentAddress();
   152	        if (aiAgent == msg.sender)             revert SelfBondNotAllowed();
   153	
   154	        IDENTITY_REGISTRY.registerAgent(agentURI, agentType, capabilitiesHash);
   155	        bondId = PARTNERSHIP_BONDS.createBond{value: msg.value}(aiAgent, partnershipType);
   156	
   157	        emit OnboardedWithBond(msg.sender, aiAgent, bondId, msg.value, agentType, partnershipType);
   158	    }
   159	
   160	    function targets() external pure returns (
   161	        address identityRegistry, address partnershipBonds,
   162	        uint256 minStakeWei, uint256 baseChainId
   161	    ) {
   162	        return (address(IDENTITY_REGISTRY), address(PARTNERSHIP_BONDS), MIN_STAKE_WEI, BASE_CHAIN_ID);
   163	    }
   164	
   165	    // INTENTIONALLY NO receive() AND NO fallback().
   166	}
```

*(Full source: 281 lines — includes 103 lines of security documentation at the top)*

### 3.3 Line-by-Line Walkthrough

| Lines | What happens |
|-------|-------------|
| 1–2 | MIT licence; `^0.8.24` — permits 0.8.24 and later patch releases (unlike StreetCred's exact pin). |
| 4–103 | Extensive block comment (the longest in any Vaultfire contract) documenting seven security principles with references to real-world exploits and the Ethereum.org Pectra delegation guidelines. Notable: references the OpenZeppelin Bug Digest #5 (Sept 2025) fallback exploit and the MetaMask Cyfrin-audited stateless delegator pattern. |
| 105–111 | `IIdentityRegistry` — minimal interface: only `registerAgent` with `(agentURI, agentType, capabilitiesHash)`. |
| 113–116 | `IPartnershipBonds` — `createBond(aiAgent, partnershipType)` as `payable`, returns `uint256` bond ID. |
| 120 | Contract declaration — no constructor args, no base contracts, no state variables. |
| 123–124 | `IDENTITY_REGISTRY` — Base mainnet address as `public constant`. Hardcoded in bytecode; cannot be redirected. |
| 127–128 | `PARTNERSHIP_BONDS` — Base mainnet address as `public constant`. |
| 135 | `MIN_STAKE_WEI = 1` — minimal on-chain requirement: any nonzero ETH. Documents that the commonly cited "0.005 ETH" was a UX recommendation, never an on-chain requirement. |
| 139 | `BASE_CHAIN_ID = 8453` — Base mainnet. Used for cross-chain replay protection. |
| 141–158 | `OnboardedWithBond` event — three indexed fields (EOA, aiAgent, bondId) plus stake amount and free-form type strings. |
| 160–178 | Error definitions — six custom errors, each with rich documentation of the failure reason. |
| 202–245 | `onboardAndBond` — the single entry point. Validation order: chain ID check first (fastest fail), then stake, then string emptiness, then address validity. All checks precede the first external call (checks-effects-interactions pattern). Step 1 calls `IDENTITY_REGISTRY.registerAgent`; Step 2 calls `PARTNERSHIP_BONDS.createBond{value: msg.value}` forwarding the entire ETH value into the bond. Emits `OnboardedWithBond`. |
| 253–269 | `targets()` — pure view returning all four constants. Explicitly designed for frontend/Basescan pre-delegation verification. |
| 271–280 | Comment block explaining the deliberate absence of `receive()` and `fallback()`. |

### 3.4 State Variables

**None.** The contract has zero storage slots — no `SSTORE` ever occurs. This is the "Stateless Delegator" pattern.

The immutable-equivalent data lives as `constant` values baked into bytecode:

| Constant | Type | Value |
|----------|------|-------|
| `IDENTITY_REGISTRY` | `IIdentityRegistry` | `0xa7BD20bf5De63df949cA5Be2F20835978eCba81A` |
| `PARTNERSHIP_BONDS` | `IPartnershipBonds` | `0x01C479F0c039fEC40c0Cf1c5C921bab457d57441` |
| `MIN_STAKE_WEI` | `uint256` | `1` (any nonzero wei) |
| `BASE_CHAIN_ID` | `uint256` | `8453` |

### 3.5 Functions — Full Behavioral Specification

**`onboardAndBond(agentURI, agentType, capabilitiesHash, aiAgent, partnershipType) payable → uint256 bondId`**  
External, `payable`. The sole entry point. Under EIP-7702 delegation, `msg.sender` is the EOA being onboarded. Validation sequence (fail-fast order):
1. `block.chainid != BASE_CHAIN_ID` → `WrongChain`
2. `msg.value < 1` → `StakeBelowMinimum`
3. `bytes(agentURI).length == 0` → `EmptyAgentURI`
4. `bytes(partnershipType).length == 0` → `EmptyPartnershipType`
5. `aiAgent == address(0)` → `ZeroAgentAddress`
6. `aiAgent == msg.sender` → `SelfBondNotAllowed`

Then: calls `IDENTITY_REGISTRY.registerAgent(agentURI, agentType, capabilitiesHash)` — this registers `msg.sender` (the EOA) in the identity registry. Then calls `PARTNERSHIP_BONDS.createBond{value: msg.value}(aiAgent, partnershipType)` — this creates a bond with the EOA as the `human` party and `aiAgent` as the counterparty, staking `msg.value`. Returns the bond ID from the bonds contract.

**`targets() → (address, address, uint256, uint256)`**  
External `pure`. Returns the four constants for pre-delegation verification by frontends and Basescan readers.

**No `receive()` or `fallback()`.**  
Plain ETH transfers to a delegated EOA revert. Calls with non-matching selectors revert. Both are intentional safety decisions documented in the inline comments.

### 3.6 Math — EIP-7702 Delegation Mechanics

EIP-7702 defines an "authorization tuple" `(chain_id, address, nonce)` that an EOA signs with its private key. The signed tuple is included in a new transaction type (Type 4). Upon execution, the EVM sets the account code of the EOA to `DELEGATECALL target → address`. Subsequent calls to the EOA run the delegate contract's code with:
- `address(this)` = the EOA's address
- `msg.sender` = the original caller (in `onboardAndBond`, the EOA calls itself, so `msg.sender` is the EOA)
- Storage reads/writes affect the EOA's storage slots

The delegation persists across transactions until explicitly revoked (by signing an authorization to `address(0)`). The user flow is:

\[
\text{User} \xrightarrow{\text{sign auth}}\ [\text{chain\_id}=8453, \text{addr}=\text{BatchOnboarder}, \text{nonce}=N]
\]
\[
\xrightarrow{\text{call}} \text{EOA.onboardAndBond}(\ldots)
\xrightarrow{\text{revoke}} [\text{chain\_id}=8453, \text{addr}=0x0, \text{nonce}=N+1]
\]

The `block.chainid == BASE_CHAIN_ID` guard prevents execution on any chain other than Base, even if the user erroneously signed with `chain_id = 0` (any-chain) in their authorization tuple.

### 3.7 Events

| Event | When fired | Fields |
|-------|-----------|--------|
| `OnboardedWithBond` | On successful `onboardAndBond` | `onboardedEoa` (indexed), `aiAgent` (indexed), `bondId` (indexed), `stakeAmount`, `agentType`, `partnershipType` |

The three indexed fields allow indexers to efficiently filter by EOA, AI agent counterparty, or bond ID.

### 3.8 Security Invariants

1. **No storage — no init attack:** Without storage slots, there is no `initialize()` or `init()` function that a frontrunner could call with malicious parameters to hijack a freshly delegated account.
2. **No arbitrary execution:** Unlike `execute(target, value, data)` patterns in full smart wallets, `onboardAndBond` can only call two hardcoded addresses. An attacker who convinces a user to delegate cannot redirect the calls.
3. **Cross-chain replay protection:** `block.chainid == BASE_CHAIN_ID` rejects execution on all non-Base chains, even if a `chain_id = 0` authorization is replayed.
4. **Self-bond prevention:** `aiAgent == msg.sender` reverts with `SelfBondNotAllowed`. The underlying `PartnershipBondsV2` also enforces this; the check here provides an earlier, clearer error.
5. **No ETH stranding:** The entire `msg.value` is forwarded to the bond contract in the same call. No ETH is held by the delegate.
6. **No fallback ETH drain:** Absence of `receive()` and `fallback()` prevents the attack vector documented in OZ Bug Digest #5 where an auto-triggered fallback drained a delegated EOA on plain ETH transfers.

### 3.9 Standards Compliance

- **EIP-7702** (Pectra): The contract is designed as a "Stateless Delegator" per the Pectra delegation spec. It respects: (a) no state prevents init-frontrunning; (b) no fallback prevents ETH drain via plain transfer; (c) no relayer dependency; (d) chain ID enforcement. It does NOT implement `execute(target, value, data)` — intentionally narrower than general-purpose smart wallets.
- No ERC-721/ERC-20 compliance — this is a utility contract, not a token.

### 3.10 Dependencies + Rebuild Recipe

**External dependencies:** None at the contract level. Build tooling uses `forge-std`.

**Build:**
```bash
cd sprint-7702
forge build
```

**Deploy (generates bytecode for user to sign-and-send via Base App):**
```bash
forge script script/Deploy.s.sol --rpc-url https://mainnet.base.org
# Or with BROADCAST=true env var to actually deploy:
BROADCAST=true forge script script/Deploy.s.sol --rpc-url https://mainnet.base.org --broadcast
```

**Test:**
```bash
forge test --fork-url https://mainnet.base.org
```

---

## 4. VaultfireBatchOnboarder.t.sol

**File:** `sprint-7702/test/VaultfireBatchOnboarder.t.sol`  
**Path:** `/home/user/workspace/sprint-7702/test/VaultfireBatchOnboarder.t.sol`  
**Category:** Sprint-7702 — Test Suite  
**Compiler:** `^0.8.24`  
**SHA-256:** `dcb715cd9b70efa541b0228ad44a6989bd12ef773a33c236998454d1cde4f50d`

### 4.1 Purpose

Full Foundry test suite for `VaultfireBatchOnboarder`. Tests run on a Base mainnet fork. A key challenge documented in the file: Foundry's EIP-7702 cheatcodes (`vm.signDelegation`, `vm.attachDelegation`) were added in 2025 nightly builds. The suite provides two delegation simulation paths: the native 7702 cheatcode path, and a fallback `vm.etch` path that achieves identical observable behavior for older forge versions.

### 4.2 Test Structure

**Setup:** Forks Base mainnet. Deploys fresh `VaultfireBatchOnboarder`. Sets up Alice EOA (`USER_PK = 0xA11CE`, `vm.addr(0xA11CE)`) with 1 ETH.

**Delegation helper `_delegate(pk, target)`:** Tries `vm.signDelegation` + `vm.attachDelegation` via a guarded external call with try/catch. Falls back to `vm.etch(eoa, target.code)` for older forge versions.

| Test | Assertion |
|------|-----------|
| `test_onboardAndBond_happyPath_explicitCounterparty` | Full happy path: agent registered (verified via `getAgent`), bond created (verified via `bonds(bondId)`), `OnboardedWithBond` event fires with correct payload including `bondId = nextBondIdBefore`. |
| `test_revert_zeroAgentAddress` | `ZeroAgentAddress` on `aiAgent = address(0)`. |
| `test_revert_selfBondNotAllowed` | `SelfBondNotAllowed` on `aiAgent = user`. |
| `test_onboardAndBond_minStake_oneWei` | Bond created with 1 wei stake; `stakeAmt = 1` confirmed. Documents that the "0.005 ETH minimum" was never on-chain. |
| `test_revert_stakeBelowMinimum` | `StakeBelowMinimum(0, 1)` on `msg.value = 0`. |
| `test_revert_emptyAgentURI` | `EmptyAgentURI`. |
| `test_revert_emptyPartnershipType` | `EmptyPartnershipType`. |
| `test_revert_wrongChain` | `WrongChain(8453, 1)` after `vm.chainId(1)`. |
| `test_plainEthTransferToDelegatedEOA_reverts` | Plain `call{value: 0.1 ether}("")` to delegated EOA returns `ok = false`. |
| `test_unknownSelectorOnDelegatedEOA_reverts` | `call(abi.encodeWithSignature("doesNotExist()"))` returns `ok = false`. |
| `test_targets_returnsImmutableConstants` | `targets()` returns the four expected constant values. |
| `test_twoUsersOnboard_independently` | Two distinct EOAs each delegate, onboard, and bond with each other. `b2 > b1` (ascending bond IDs). Each bond's `human` matches the correct EOA. |

---

## 5. Deploy.s.sol

**File:** `sprint-7702/script/Deploy.s.sol`  
**Path:** `/home/user/workspace/sprint-7702/script/Deploy.s.sol`  
**Category:** Sprint-7702 — Deploy Script  
**Compiler:** `^0.8.24`  
**SHA-256:** `26cb6b49ece5a98b9ddfd7363733a0d55d1f6eacbbfc77467d60668b341be2a0`

### 5.1 Purpose

A Foundry `Script` contract that serves two purposes: (A) prints the creation bytecode and size to the console for manual deployment via a wallet UI (the primary workflow for mobile-only Base App users); (B) deploys the contract if run with `--broadcast` and `BROADCAST=true` environment variable.

### 5.2 Source

```solidity
 1	// SPDX-License-Identifier: MIT
 2	pragma solidity ^0.8.24;
 3	import {Script, console2} from "forge-std/Script.sol";
 4	import {VaultfireBatchOnboarder} from "../src/VaultfireBatchOnboarder.sol";
 5	
 6	contract Deploy is Script {
 7	    function run() external {
 8	        bytes memory creationCode = type(VaultfireBatchOnboarder).creationCode;
 9	        console2.log("=== VaultfireBatchOnboarder deploy ===");
 10	        console2.log("Target chain id (Base): 8453");
 11	        console2.log("Creation bytecode size (bytes):", creationCode.length);
 12	        console2.logBytes(creationCode);
13	
14	        if (vm.envOr("BROADCAST", false)) {
15	            vm.startBroadcast();
16	            VaultfireBatchOnboarder onboarder = new VaultfireBatchOnboarder();
17	            vm.stopBroadcast();
18	            console2.log("Deployed at:", address(onboarder));
19	        }
20	    }
21	}
```

### 5.3 Walkthrough

- Line 8: `type(VaultfireBatchOnboarder).creationCode` — Solidity built-in to get the full constructor+runtime bytecode as a `bytes` value.
- Lines 9–12: Print the bytecode hex — users copy this and send a zero-address contract creation transaction from their wallet.
- Lines 14–19: Conditional broadcast via `vm.envOr("BROADCAST", false)` — allows the same script to serve as a dry-run (print only) or a live deployment. Uses `vm.startBroadcast()` / `vm.stopBroadcast()` so all transactions in the block are sent atomically with the configured private key.

**Usage:**
```bash
# Dry-run (print bytecode):
forge script script/Deploy.s.sol --rpc-url https://mainnet.base.org

# Live deploy:
BROADCAST=true forge script script/Deploy.s.sol \
  --rpc-url https://mainnet.base.org \
  --private-key $DEPLOYER_KEY \
  --broadcast
```

---

## 6. ITeleporterMessenger.sol

**File:** `theloopbreaker-site/contracts/teleporter/ITeleporterMessenger.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/contracts/teleporter/ITeleporterMessenger.sol`  
**Category:** Teleporter messenger interface  
**Compiler:** `^0.8.25`  
**SHA-256:** `5fc6538e48c5f1612f778fc1fd2ca709462174f1ab84156cc7a3d649dc13472f`

### 6.1 Purpose

Vaultfire's local copy of the Avalanche ICM (Interchain Messaging) `ITeleporterMessenger` interface, compatible with `ava-labs/icm-contracts`. This interface defines the five global structs used throughout the Teleporter cross-chain messaging system and the send/receive/query function surface. All Vaultfire bridge contracts import this file.

### 6.2 Full Source Code

```solidity
  1	// SPDX-License-Identifier: MIT
  2	pragma solidity ^0.8.25;
  3	
  4	struct TeleporterMessageReceipt {
  5	    uint256 receivedMessageNonce;
  6	    address relayerRewardAddress;
  7	}
  8	
  9	struct TeleporterMessageInput {
 10	    bytes32 destinationBlockchainID;
 11	    address destinationAddress;
 12	    TeleporterFeeInfo feeInfo;
 13	    uint256 requiredGasLimit;
 14	    address[] allowedRelayerAddresses;
 15	    bytes message;
 16	}
 17	
 18	struct TeleporterMessage {
 19	    uint256 messageNonce;
 20	    address originSenderAddress;
 21	    bytes32 destinationBlockchainID;
 22	    address destinationAddress;
 23	    uint256 requiredGasLimit;
 24	    address[] allowedRelayerAddresses;
 25	    TeleporterMessageReceipt[] receipts;
 26	    bytes message;
 27	}
 28	
 29	struct TeleporterFeeInfo {
 30	    address feeTokenAddress;
 31	    uint256 amount;
 32	}
 33	
 34	interface ITeleporterMessenger {
 35	    event SendCrossChainMessage(
 36	        bytes32 indexed messageID,
 37	        bytes32 indexed destinationBlockchainID,
 38	        TeleporterMessage message,
 39	        TeleporterFeeInfo feeInfo
 40	    );
 41	    event ReceiveCrossChainMessage(
 42	        bytes32 indexed messageID,
 43	        bytes32 indexed sourceBlockchainID,
 44	        address indexed deliverer,
 45	        address rewardRedeemer,
 46	        TeleporterMessage message
 47	    );
 48	    function sendCrossChainMessage(TeleporterMessageInput calldata messageInput) external returns (bytes32);
 49	    function retrySendCrossChainMessage(TeleporterMessage calldata message) external;
 50	    function addFeeAmount(bytes32 messageID, address feeTokenAddress, uint256 additionalFeeAmount) external;
 51	    function receiveCrossChainMessage(uint32 messageIndex, address relayerRewardAddress) external;
 52	    function retryMessageExecution(bytes32 sourceBlockchainID, TeleporterMessage calldata message) external;
 53	    function messageReceived(bytes32 messageID) external view returns (bool);
 54	    function getMessageHash(bytes32 messageID) external view returns (bytes32);
 55	    function getNextMessageID(bytes32 destinationBlockchainID) external view returns (bytes32);
 56	}
```

### 6.3 Struct Reference

| Struct | Field | Purpose |
|--------|-------|---------|
| `TeleporterMessageReceipt` | `receivedMessageNonce` | The nonce of the received message being acknowledged. |
| | `relayerRewardAddress` | Address entitled to the relayer fee reward. |
| `TeleporterMessageInput` | `destinationBlockchainID` | Teleporter `bytes32` blockchain ID of the target chain. For non-Avalanche chains: `keccak256(abi.encodePacked(chainId))`. |
| | `destinationAddress` | Contract address on the destination chain (the `ITeleporterReceiver`). |
| | `feeInfo` | `TeleporterFeeInfo` — fee token address + amount. Vaultfire uses `{address(0), 0}` (no fee). |
| | `requiredGasLimit` | Gas that must be available on the destination for message execution. |
| | `allowedRelayerAddresses` | Whitelist of relayers permitted to deliver this message. Empty = any relayer. |
| | `message` | ABI-encoded payload — in Vaultfire's case, an encoded `BridgeMessage`. |
| `TeleporterMessage` | All `TeleporterMessageInput` fields + `messageNonce`, `originSenderAddress`, `receipts[]`. | Full message as stored and delivered. |
| `TeleporterFeeInfo` | `feeTokenAddress`, `amount` | ERC-20 fee token and amount. `address(0)` = native asset fee. |

### 6.4 Function Reference

| Function | Behavior |
|----------|----------|
| `sendCrossChainMessage(input)` | Submits a new cross-chain message. Returns the `messageID` (a `bytes32` hash). The off-chain Teleporter relayer network picks up the `SendCrossChainMessage` event and delivers it. |
| `retrySendCrossChainMessage(message)` | Re-submits a previously sent but undelivered message. |
| `addFeeAmount(messageID, token, amount)` | Adds additional fee to an already-sent, undelivered message to incentivize relayer pickup. |
| `receiveCrossChainMessage(index, relayerRewardAddr)` | Called by the relayer on the destination chain to deliver a message. Invokes the recipient's `receiveTeleporterMessage`. |
| `retryMessageExecution(sourceBlockchainID, message)` | Retries execution of a delivered but reverted message. |
| `messageReceived(messageID)` | Returns `true` if the message has been processed. |
| `getMessageHash(messageID)` | Returns the hash used for deduplication. |
| `getNextMessageID(destBlockchainID)` | Returns the next message ID for a given destination chain. |

---

## 7. ITeleporterReceiver.sol

**File:** `theloopbreaker-site/contracts/teleporter/ITeleporterReceiver.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/contracts/teleporter/ITeleporterReceiver.sol`  
**Category:** Teleporter receiver interface  
**Compiler:** `^0.8.25`  
**SHA-256:** `3e42821a0d837696c24f9356dd5b9cbd0da8291fa50d0d3b1ff19d4f05dc1538`

### 7.1 Source

```solidity
  1	// SPDX-License-Identifier: MIT
  2	pragma solidity ^0.8.25;
  3	
  4	interface ITeleporterReceiver {
  5	    function receiveTeleporterMessage(
  6	        bytes32 sourceBlockchainID,
  7	        address originSenderAddress,
  8	        bytes calldata message
  9	    ) external;
  10	}
```

### 7.2 Purpose

Any contract that wants to receive ICM messages must implement `ITeleporterReceiver`. The `TeleporterMessenger` on the destination chain calls `receiveTeleporterMessage` with:
- `sourceBlockchainID`: Teleporter bytes32 ID of the originating chain — used for verification.
- `originSenderAddress`: The contract address that called `sendCrossChainMessage` on the source chain.
- `message`: The raw ABI-encoded payload — in Vaultfire's case, a `BridgeMessage` struct.

Both `VaultfireTeleporterBridge` and `TrustPortabilityExtension` implement this interface and verify both `sourceBlockchainID == remoteBlockchainID` and `originSenderAddress == remoteBridgeAddress` before processing.

---

## 8. VaultfireTeleporterBridge.sol

**File:** `theloopbreaker-site/contracts/teleporter/VaultfireTeleporterBridge.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/contracts/teleporter/VaultfireTeleporterBridge.sol`  
**Category:** TeleporterBridge V1  
**Compiler:** `^0.8.25`  
**SHA-256:** `7c6eb770804f75b3b1041bea54e19f8f4f133956b7d838e1496e37364680234f`

### 8.1 Purpose

`VaultfireTeleporterBridge` is the V1 cross-chain trust portability bridge for the Vaultfire protocol, built for the Avalanche Build Games 2026. It enables AI agent trust state — registration, bonds, reputation, validation — to be synchronized between Base mainnet and Avalanche C-Chain. The contract deploys identically on both chains; on Avalanche it receives messages natively via `ITeleporterReceiver`, while on Base it uses an off-chain authorized relayer.

### 8.2 Architecture

```
Base chain                    Relayer / ICM                 Avalanche C-Chain
─────────────────────────     ──────────────────────────   ─────────────────────────
VaultfireTeleporterBridge  ──►  Off-chain relayer OR     ──►  VaultfireTeleporterBridge
  sendAgentRegistration()       Teleporter ICM native          receiveTeleporterMessage()
  sendPartnershipBond()                                         relayMessage()
  sendReputation()                                              _processInboundMessage()
  ...                                                           syncedAgents[agent] = p
```

On chains with a deployed `TeleporterMessenger` (Avalanche and its L1s), outbound messages are sent via `ITeleporterMessenger.sendCrossChainMessage`. On chains without native ICM (Base), the `MessageSent` event is emitted and an authorized off-chain relayer calls `relayMessage()` on the destination chain.

### 8.3 Message Types (Enum `MessageType`)

| Value | Name | Payload struct |
|-------|------|---------------|
| 0 | `SYNC_AGENT_REGISTRATION` | `AgentRegistrationPayload` |
| 1 | `SYNC_PARTNERSHIP_BOND` | `PartnershipBondPayload` |
| 2 | `SYNC_ACCOUNTABILITY_BOND` | `AccountabilityBondPayload` |
| 3 | `SYNC_REPUTATION` | `ReputationPayload` |
| 4 | `SYNC_VALIDATION` | `ValidationPayload` |

### 8.4 Struct Reference

**`BridgeMessage` (envelope for all messages):**

| Field | Type | Purpose |
|-------|------|---------|
| `messageType` | `MessageType` | Discriminator for payload decoding. |
| `sourceChainId` | `uint256` | EVM chain ID of the origin. |
| `nonce` | `uint256` | Monotonically increasing per-bridge counter for replay protection. |
| `timestamp` | `uint256` | `block.timestamp` on the source chain. |
| `sender` | `address` | `msg.sender` of the send function call. |
| `payload` | `bytes` | ABI-encoded type-specific struct. |

**`AgentRegistrationPayload`:** `(agentAddress, agentURI, agentType, capabilitiesHash, registeredAt)`  
**`PartnershipBondPayload`:** `(bondId, human, aiAgent, purpose, createdAt, active)`  
**`AccountabilityBondPayload`:** `(bondId, aiCompany, companyName, quarterlyRevenue, stakeAmount, createdAt, active)`  
**`ReputationPayload`:** `(agentAddress, totalFeedbacks, averageRating, verifiedFeedbacks, lastUpdated)`  
**`ValidationPayload`:** `(requestId, agentAddress, status, approvalsCount, rejectionsCount, timestamp)`

### 8.5 State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|-----------|---------|
| `owner` | `address` | mutable | Contract deployer; controls relayer whitelist, pause, remote configuration. |
| `paused` | `bool` | mutable | Emergency kill switch; blocks all send/receive operations. |
| `outboundNonce` | `uint256` | mutable | Monotonic counter for outbound messages; pre-incremented in `_sendMessage`. |
| `teleporterMessenger` | `address` | mutable | Address of the ICM messenger on this chain. `address(0)` on Base (non-Avalanche). |
| `remoteBlockchainID` | `bytes32` | mutable | Teleporter blockchain ID of the peer chain. |
| `remoteBridgeAddress` | `address` | mutable | Address of the peer bridge contract. |
| `remoteChainId` | `uint256` | mutable | EVM chain ID of the peer chain. |
| `requiredGasLimit` | `uint256` | mutable | Gas limit for Teleporter message delivery on the destination. |
| `authorizedRelayers` | `mapping(address => bool)` | mutable | Relayer whitelist. Owner is automatically added at construction. |
| `relayerList` | `address[]` | mutable | Iterable list of authorized relayers. O(n) removal in V1 (fixed to O(1) in V3). |
| `processedMessages` | `mapping(bytes32 => bool)` | mutable | Replay protection: set to `true` for each processed message hash. |
| `lastProcessedNonce` | `uint256` | mutable | Highest inbound nonce seen; updated on every received message. |
| `syncedAgents` | `mapping(address => AgentRegistrationPayload)` | mutable | Agents synced from remote chain. |
| `syncedAgentList` | `address[]` | mutable | Enumerable list of synced agent addresses. |
| `syncedPartnershipBonds` | `mapping(uint256 => PartnershipBondPayload)` | mutable | Partnership bonds synced from remote. |
| `syncedAccountabilityBonds` | `mapping(uint256 => AccountabilityBondPayload)` | mutable | Accountability bonds synced from remote. |
| `syncedReputations` | `mapping(address => ReputationPayload)` | mutable | Reputation data synced from remote. |
| `syncedValidations` | `mapping(uint256 => ValidationPayload)` | mutable | Validation results synced from remote. |

### 8.6 Functions — Full Behavioral Specification

**Admin functions (all `onlyOwner`):**
- `transferOwnership(newOwner)` — transfers ownership; zero-address guard; emits `OwnershipTransferred`.
- `setRemoteBridge(blockchainID, address, chainId)` — configures the peer bridge; zero-address guard; emits `RemoteConfigured`.
- `setRequiredGasLimit(gasLimit)` — updates gas limit for ICM delivery.
- `setTeleporterMessenger(messenger)` — updates ICM messenger address (needed for deployment on new chains).
- `addRelayer(relayer)` — adds relayer to whitelist; zero-address guard; checks not already authorized; emits `RelayerAdded`. **V1 issue:** O(n) search on removal.
- `removeRelayer(relayer)` — removes from whitelist; O(n) linear scan to find index; swap-and-pop; emits `RelayerRemoved`.
- `pause()` / `unpause()` — toggles pause state; emits `BridgePaused` / `BridgeUnpaused`.

**Outbound send functions (all `onlyAuthorizedRelayer whenNotPaused`):**  
All five `send*` functions (added `onlyAuthorizedRelayer` in audit fix HIGH-002 to prevent arbitrary trust injection) construct a typed payload struct and delegate to `_sendMessage`. Return the outbound nonce.

**`receiveTeleporterMessage(sourceBlockchainID, originSenderAddress, message) external override onlyTeleporter whenNotPaused`**  
ICM entry point — called by `TeleporterMessenger` on Avalanche. Verifies `sourceBlockchainID == remoteBlockchainID` and `originSenderAddress == remoteBridgeAddress`. Calls `_processInboundMessage`.

**`relayMessage(encodedMessage) external onlyAuthorizedRelayer whenNotPaused`**  
Off-chain relayer entry point for Base↔Avalanche transport. Calls `_processInboundMessage` directly. Note: in V1, there is no ECDSA signature requirement on the encoded message — this is the CRIT-02 vulnerability fixed in V3.3.

**View functions:**  
`getSyncedAgent`, `getSyncedPartnershipBond`, `getSyncedAccountabilityBond`, `getSyncedReputation`, `getSyncedValidation` — return full payload structs by key.  
`getSyncedAgentCount`, `getSyncedPartnershipBondCount`, etc. — return array lengths.  
`isAgentRecognized(agent)` — returns `true` if `syncedAgents[agent].agentAddress != address(0)`.

**Internal:**  
`_sendMessage(type, payload)` — pre-increments `outboundNonce`, constructs `BridgeMessage`, ABI-encodes, calls `_sendViaTeleporter` if ICM is available (otherwise event-only), emits `MessageSent`.  
`_sendViaTeleporter(encoded)` — constructs `TeleporterMessageInput` with `feeInfo = {address(0), 0}` (no fee), calls `ITeleporterMessenger.sendCrossChainMessage`.  
`_processInboundMessage(encoded)` — decodes `BridgeMessage`, checks hash replay protection, updates `lastProcessedNonce`, verifies `sourceChainId`, routes to type-specific handler, emits `MessageReceived`.  
Type-specific handlers: `_handleAgentRegistration`, `_handlePartnershipBond`, `_handleAccountabilityBond`, `_handleReputation`, `_handleValidation` — each decodes the payload, upserts the corresponding synced mapping, appends to the tracking list, and emits the type-specific sync event.

### 8.7 Cross-Chain Message Flow

**Source chain (Base) → Destination chain (Avalanche):**

1. Authorized relayer calls `sendAgentRegistration(agentAddress, agentURI, ...)` on the Base bridge.
2. `_sendMessage` increments `outboundNonce` to N, constructs `BridgeMessage{nonce: N, sourceChainId: 8453, ...}`, ABI-encodes it.
3. Since `teleporterMessenger = address(0)` on Base, `_sendViaTeleporter` is NOT called. The `MessageSent(N, type, hash, remoteChainId)` event is emitted.
4. Off-chain relayer monitors `MessageSent` events on Base. It takes the encoded message and submits `relayMessage(encodedMessage)` on the Avalanche bridge.
5. Avalanche bridge's `relayMessage` verifies relayer authorization and calls `_processInboundMessage`.
6. `_processInboundMessage` computes `keccak256(encoded)`, checks `processedMessages[hash]` (replay guard), decodes `BridgeMessage`, verifies `sourceChainId == 8453`, routes to `_handleAgentRegistration`.
7. `_handleAgentRegistration` upserts `syncedAgents[agentAddress] = payload`, appends to `syncedAgentList`, emits `AgentSynced`.

**Source chain (Avalanche) → Destination chain (Base):**

1. Same send path, but since `teleporterMessenger` is set on Avalanche, `_sendViaTeleporter` calls `ITeleporterMessenger.sendCrossChainMessage`.
2. ICM relayer network delivers the message natively.
3. ICM calls `receiveTeleporterMessage(sourceBlockchainID, originSenderAddress, encoded)` on the Base bridge.
4. `receiveTeleporterMessage` verifies source and sender, calls `_processInboundMessage`.

### 8.8 Events

| Event | When fired | Fields |
|-------|-----------|--------|
| `MessageSent` | On every `_sendMessage` | `nonce` (indexed), `messageType` (indexed), `messageHash`, `destinationChainId` |
| `MessageReceived` | On every `_processInboundMessage` | `nonce` (indexed), `messageType` (indexed), `messageHash`, `sourceChainId` |
| `AgentSynced` | On `_handleAgentRegistration` | `agentAddress` (indexed), `agentType`, `sourceChainId` |
| `PartnershipBondSynced` | On `_handlePartnershipBond` | `bondId` (indexed), `human`, `aiAgent`, `sourceChainId` |
| `AccountabilityBondSynced` | On `_handleAccountabilityBond` | `bondId` (indexed), `aiCompany`, `sourceChainId` |
| `ReputationSynced` | On `_handleReputation` | `agentAddress` (indexed), `averageRating`, `sourceChainId` |
| `ValidationSynced` | On `_handleValidation` | `requestId` (indexed), `agentAddress`, `status`, `sourceChainId` |
| `RelayerAdded` / `RelayerRemoved` | On relayer changes | `relayer` (indexed) |
| `BridgePaused` / `BridgeUnpaused` | On pause state changes | `by` (indexed) |
| `RemoteConfigured` | On `setRemoteBridge` | `remoteBlockchainID`, `remoteBridgeAddress`, `remoteChainId` |
| `OwnershipTransferred` | On `transferOwnership` | `previousOwner` (indexed), `newOwner` (indexed) |

### 8.9 Security Invariants

1. **Relayer whitelist:** Only `authorizedRelayers` can call send functions and `relayMessage`. Owner is automatically a relayer.
2. **Replay protection:** `processedMessages[keccak256(encoded)]` is checked before processing any inbound message. Second delivery of the identical bytes always reverts.
3. **Source chain verification:** `_processInboundMessage` checks `bridgeMsg.sourceChainId == remoteChainId`. A message claiming to be from a different chain reverts.
4. **Teleporter path security:** `receiveTeleporterMessage` requires `msg.sender == teleporterMessenger` (the official ICM contract). Both `sourceBlockchainID` and `originSenderAddress` are verified.
5. **Pause mechanism:** `whenNotPaused` gates all send/receive operations. Owner can halt the bridge without destroying synced state.

**V1 Known Limitations (fixed in V3):**
- CRIT-02: `relayMessage` accepts any encoded payload without ECDSA signature verification — a rogue authorized relayer could fabricate trust data.
- HIGH-002: Send functions lacked `onlyAuthorizedRelayer` (fixed via `@custom:audit-fix` annotation).
- O(n) relayer removal via linear scan (fixed to O(1) swap-and-pop in V3).
- No array size cap — unbounded arrays can cause OOG (fixed in V3 with `MAX_SYNCED_ENTRIES = 10000`).

### 8.10 Dependencies + Rebuild Recipe

```bash
cd theloopbreaker-site
npm install  # hardhat environment
npx hardhat compile
```

Or with Foundry (requires remappings for the relative imports):
```bash
forge build --root theloopbreaker-site
```

---

## 9. VaultfireTeleporterBridgeV3.sol

**File:** `VaultfireTeleporterBridgeV3.sol`  
**Path:** `/home/user/workspace/VaultfireTeleporterBridgeV3.sol`  
**Category:** TeleporterBridge V3.3 (partner-ready, post-audit)  
**Compiler:** `^0.8.25`  
**SHA-256:** `d285dde0562b5846c2ea622189bcffcd32bd1d88bd750ecb7d1c4ac94f32d865`

### 9.1 Purpose

V3.3 of the cross-chain bridge — the production-ready successor to V1. This version incorporates all audit remediations across three audit rounds (v3.1, v3.3, v3.3-r2). Key improvements:

- **CRIT-02 fix:** `relayMessage` now requires a 65-byte ECDSA signature from `remoteBridgeSigner` over `keccak256(encodedMessage)`, using EIP-191 `eth_sign` format. This cryptographically authenticates the source of every off-chain-relayed message.
- **Array caps:** All synced tracking arrays are capped at `MAX_SYNCED_ENTRIES = 10000` to prevent OOG on full iteration.
- **O(1) relayer management:** Index mapping `_relayerIndex` enables O(1) swap-and-pop removal.
- **Multi-asset awareness:** `PartnershipBondPayload` and `AccountabilityBondPayload` now include `stakeToken address` (ERC-20 token or `address(0)` for native).
- **Bond deactivation messages:** New `SYNC_BOND_DEACTIVATION` message type (value 5) and `BondDeactivationPayload` allow active-status changes to be synchronized cross-chain.
- **Reputation timestamp guard:** `syncedReputationTimestamp[agent]` prevents stale updates from overwriting newer data.
- **Sender verification:** `_processInboundMessage` now checks `bridgeMsg.sender == remoteBridgeAddress` (HIGH-BRIDGE-02).

### 9.2 Changes from V1

| Category | V1 | V3.3 |
|----------|----|------|
| Relayer-path auth | None — relayer forwards raw bytes | ECDSA signature required (`remoteBridgeSigner`) |
| Array caps | None — unbounded growth | `MAX_SYNCED_ENTRIES = 10000`; `SyncedCapReached` event on overflow |
| Relayer removal | O(n) linear scan | O(1) swap-and-pop with `_relayerIndex` |
| Bond payloads | No stake token | `stakeToken address` field |
| Bond deactivation | Not supported | `SYNC_BOND_DEACTIVATION` (type 5) |
| Reputation updates | Accept any update | Reject stale via `syncedReputationTimestamp` |
| Sender verification | Not checked inside `_processInboundMessage` | `bridgeMsg.sender == remoteBridgeAddress` |
| Last relayer removal | Allowed | Blocked (`relayerList.length > 1` guard) |
| Relayer replay guard | `processedMessages` only | `relayerMessageHashes` + `processedMessages` |

### 9.3 CRIT-02 Fix — ECDSA Signature Verification (Lines 597–637)

```solidity
function relayMessage(
    bytes calldata encodedMessage,
    bytes calldata signature
) external onlyAuthorizedRelayer whenNotPaused {
    require(remoteBridgeSigner != address(0), "signer not configured");

    bytes32 msgHash = keccak256(encodedMessage);
    require(!relayerMessageHashes[msgHash], "relayer message replayed");
    relayerMessageHashes[msgHash] = true;

    require(signature.length == 65, "bad signature length");
    bytes32 ethSignedHash = keccak256(
        abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)
    );
    uint8 v;  bytes32 r;  bytes32 s;
    assembly {
        r := calldataload(signature.offset)
        s := calldataload(add(signature.offset, 32))
        v := byte(0, calldataload(add(signature.offset, 64)))
    }
    if (v < 27) v += 27;
    address recovered = ecrecover(ethSignedHash, v, r, s);
    require(recovered != address(0) && recovered == remoteBridgeSigner,
        "invalid bridge signature");

    _processInboundMessage(encodedMessage);
}
```

**How it works:** The remote bridge (on the source chain) signs `keccak256(encodedMessage)` using EIP-191 personal_sign format (`"\x19Ethereum Signed Message:\n32" || hash`). The 65-byte signature (r‖s‖v, packed) is passed alongside the message. The receiving bridge recovers the signer address via `ecrecover`. The signed message hash is also stored in `relayerMessageHashes` as a secondary replay guard independent of `processedMessages`.

**Setup requirement:** `setRemoteBridgeSigner(signerAddress)` must be called by the owner before any `relayMessage` call is accepted. Setting it back to `address(0)` disables the relayer path entirely.

### 9.4 Bond Deactivation Flow (HIGH-18 Fix)

When a bond is deactivated on the source chain, the relayer calls either:
- `sendBondDeactivation` (via a message of type `SYNC_BOND_DEACTIVATION` = 5)
- Or directly `notifyPartnershipBondDeactivated(bondId)` / `notifyAccountabilityBondDeactivated(bondId)`

The local `notifyBondDeactivated` functions (`onlyAuthorizedRelayer whenNotPaused`) set `active = false` on the locally stored payload and emit `BondDeactivationSynced`. The incoming message handler `_handleBondDeactivation` does the same for cross-chain notifications.

### 9.5 State Variables (V3.3 Additions to V1)

| Variable | Type | Purpose |
|----------|------|---------|
| `MAX_SYNCED_ENTRIES` | `uint256 constant` | `10000` — cap on all synced tracking arrays. |
| `MAX_RELAYERS` | `uint256 constant` | `50` — cap on relayer whitelist. |
| `_relayerIndex` | `mapping(address => uint256)` | Index in `relayerList` for O(1) swap-and-pop. |
| `remoteBridgeSigner` | `address` | ECDSA signer address for relayer-path authentication. |
| `relayerMessageHashes` | `mapping(bytes32 => bool)` | Secondary replay guard for relayer path. |
| `syncedReputationTimestamp` | `mapping(address => uint256)` | `lastUpdated` of most recently accepted reputation update. |

---

## 10. TrustPortabilityExtension.sol

**File:** `theloopbreaker-site/contracts/teleporter/TrustPortabilityExtension.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/contracts/teleporter/TrustPortabilityExtension.sol`  
**Category:** TrustPortabilityExtension — cross-chain trust/VNS/ZK  
**Compiler:** `^0.8.25`  
**SHA-256:** `19707ed2faaf8e9d66f47b1685ffbda6149ef8ffccebc45e1d78143290679b37`

### 10.1 Purpose

`TrustPortabilityExtension` extends the Teleporter bridge with three message types that were not in the original `VaultfireTeleporterBridge`: computed trust tiers, VNS name bindings, and ZK attestations. The design goal — branded "Write Once, Trust Everywhere" — is that an agent verified on Base (tier = Exemplary, score = 95) should be recognized as Exemplary on Avalanche without re-staking or re-verifying.

This contract stands alone (does not inherit from `VaultfireTeleporterBridge`) but follows the identical architectural pattern: outbound via ICM or off-chain relayer, inbound via `ITeleporterReceiver` or `relayTrustMessage`.

### 10.2 Extended Message Types

| Value | Name | Payload struct |
|-------|------|---------------|
| 0 | `SYNC_TRUST_TIER` | `TrustTierPayload` |
| 1 | `SYNC_VNS_IDENTITY` | `VNSIdentityPayload` |
| 2 | `SYNC_ZK_ATTESTATION` | `ZKAttestationPayload` |

### 10.3 Enums

**`TrustTier`:** `Unverified (0)` · `Registered (1, score 15–39)` · `Trusted (2, score 40–59)` · `HighlyTrusted (3, score 60–79)` · `Exemplary (4, score ≥ 80)` — matches the off-chain `TrustVerification.tsx` tier mapping.

**`IdentityType`:** `Human (0)` · `AIAgent (1)` · `Companion (2)` — used in VNS identity payloads.

### 10.4 Struct Reference

**`TrustTierPayload`:**

| Field | Type | Purpose |
|-------|------|---------|
| `subject` | `address` | The agent/human whose tier is being synced. |
| `tier` | `TrustTier` | Computed tier (0–4). |
| `score` | `uint256` | Raw score 0–100. |
| `activeBondCount` | `uint256` | Number of active bonds (audit trail). |
| `averageRating` | `uint256` | Reputation rating in basis points (0–10000). |
| `verifiedFeedbacks` | `uint256` | Number of verified feedback entries. |
| `computedAt` | `uint256` | Source chain `block.timestamp` when tier was computed. |
| `expiresAt` | `uint256` | `computedAt + tierValiditySeconds`; tier must be re-synced after this. |
| `sourceChainId` | `uint256` | Chain where the tier was computed. |

**`VNSIdentityPayload`:**

| Field | Purpose |
|-------|---------|
| `owner` | Address owning the VNS name. |
| `name` | Full name string (e.g. `"alice.vns"`). |
| `nameHash` | `keccak256(name)` for O(1) lookup. |
| `identityType` | `Human / AIAgent / Companion`. |
| `registeredAt` | Source chain registration timestamp. |
| `expiresAt` | Expiry (0 = permanent). |
| `sourceChainId` | Chain where name is registered. |
| `paymentAddress` | x402 payment address (defaults to `owner` if `address(0)` passed). |

**`ZKAttestationPayload`:**

| Field | Purpose |
|-------|---------|
| `prover` | Address that proved the belief. |
| `beliefHash` | `keccak256` hash of the attested belief. |
| `zkVerified` | `true` if a RISC Zero ZK proof was used; `false` = signature-only attestation. |
| `attestedAt` | Source chain attestation timestamp. |
| `sourceChainId` | Chain where proof was verified. |
| `verifierImageId` | RISC Zero guest image ID for auditability. |

### 10.5 Functions — Full Behavioral Specification

**`sendTrustTier(subject, tier, score, activeBondCount, averageRating, verifiedFeedbacks, tierValiditySeconds)`**  
External, `onlyAuthorizedRelayer whenNotPaused`. Validates: `subject != address(0)`, `score <= 100`, `tierValiditySeconds > 0`. Constructs `TrustTierPayload` with `computedAt = block.timestamp`, `expiresAt = block.timestamp + tierValiditySeconds`. Calls `_sendTrustMessage`.

**`sendVNSIdentity(owner_, name, identityType, registeredAt, expiresAt, paymentAddress)`**  
External, `onlyAuthorizedRelayer whenNotPaused`. Validates: `owner_ != address(0)`, `bytes(name).length > 0`. Computes `nameHash = keccak256(bytes(name))`. Sets `paymentAddress = owner_` if `address(0)` passed. Calls `_sendTrustMessage`.

**`sendZKAttestation(prover, beliefHash, zkVerified, attestedAt, verifierImageId)`**  
External, `onlyAuthorizedRelayer whenNotPaused`. Validates: `prover != address(0)`, `beliefHash != bytes32(0)`. Calls `_sendTrustMessage`.

**`receiveTeleporterMessage(sourceBlockchainID, originSenderAddress, message)`**  
External, `onlyTeleporter whenNotPaused`. Verifies `originSenderAddress == remoteBridgeAddress` (but notably does NOT verify `sourceBlockchainID` in this version — a potential issue vs V1 bridge's dual check). Calls `_processTrustMessage`.

**`relayTrustMessage(encoded)`**  
External, `onlyAuthorizedRelayer whenNotPaused`. Calls `_processTrustMessage`.

**`isTrustTierRecognized(subject) → (bool recognized, TrustTier tier, uint256 score)`**  
External `view`. Returns `(false, Unverified, 0)` if no record or if `expiresAt > 0 && block.timestamp > expiresAt`. Otherwise returns the stored tier and score. This expiry check is the primary mechanism preventing stale cross-chain attestations from persisting.

**`resolveVNS(name) → (address owner_, IdentityType identityType, address paymentAddress)`**  
External `view`. Hashes the name, looks up `syncedVNSByNameHash`, checks expiry, returns the binding.

**`reverseResolveVNS(subject) → string name`**  
External `view`. Looks up `syncedVNSByAddress[subject]` to get the name hash, then returns the name string. Returns empty string if not found or expired.

**`isBeliefAttested(beliefHash, prover) → (bool attested, bool zkVerified, uint256 sourceChainId)`**  
External `view`. Returns the cross-chain attestation status for a `(beliefHash, prover)` pair.

**`getProverAttestations(prover) → bytes32[] beliefHashes`**  
External `view`. Returns all belief hashes attested cross-chain by a given prover.

### 10.6 State Variables

| Variable | Type | Purpose |
|----------|------|---------|
| `owner` | `address` | Deployer; controls relayer whitelist and configuration. |
| `paused` | `bool` | Emergency kill switch. |
| `outboundNonce` | `uint256` | Monotonic outbound message counter. |
| `teleporterMessenger` | `address` | ICM messenger address. |
| `remoteBlockchainID` | `bytes32` | Teleporter blockchain ID of the peer chain. |
| `remoteBridgeAddress` | `address` | Peer contract address. |
| `remoteChainId` | `uint256` | EVM chain ID of the peer chain. |
| `requiredGasLimit` | `uint256` | Gas limit for ICM delivery. |
| `authorizedRelayers` | `mapping(address => bool)` | Relayer whitelist. |
| `processedMessages` | `mapping(bytes32 => bool)` | Replay protection. |
| `syncedTrustTiers` | `mapping(address => TrustTierPayload)` | Synced trust tiers by subject address. |
| `syncedTrustTierSubjects` | `address[]` | Enumerable list of synced subjects. |
| `syncedVNSByNameHash` | `mapping(bytes32 => VNSIdentityPayload)` | VNS bindings by name hash. |
| `syncedVNSByAddress` | `mapping(address => bytes32)` | Reverse map: address → name hash. |
| `syncedVNSNameHashes` | `bytes32[]` | Enumerable list of synced name hashes. |
| `syncedZKAttestations` | `mapping(bytes32 => mapping(address => ZKAttestationPayload))` | ZK attestations by `(beliefHash, prover)`. |
| `proverAttestations` | `mapping(address => bytes32[])` | All belief hashes attested by each prover. |

### 10.7 Cross-Chain Flow — VNS Name Portability

1. User registers `alice.vns` on the Base VaultfireNameService.
2. Authorized relayer calls `sendVNSIdentity(alice, "alice.vns", Human, registeredAt, 0, address(0))` on the Base extension.
3. `_sendTrustMessage` constructs `VNSIdentityPayload{nameHash: keccak256("alice.vns"), ...}`, wraps it in `TrustMessage`, ABI-encodes, routes via ICM or off-chain relayer.
4. On Avalanche, `_processTrustMessage` decodes, checks replay, verifies source chain, calls `_handleVNSIdentity`.
5. `_handleVNSIdentity` stores `syncedVNSByNameHash[nameHash] = payload` and `syncedVNSByAddress[alice] = nameHash`.
6. Any Avalanche contract or dApp can now call `resolveVNS("alice.vns")` → `(alice, Human, alice)` without any cross-chain call.

### 10.8 Events

| Event | Trigger | Fields |
|-------|---------|--------|
| `TrustTierSynced` | On `_handleTrustTier` | `subject` (indexed), `tier`, `score`, `sourceChainId`, `expiresAt` |
| `VNSIdentitySynced` | On `_handleVNSIdentity` | `owner` (indexed), `name`, `nameHash` (indexed), `identityType`, `sourceChainId` |
| `ZKAttestationSynced` | On `_handleZKAttestation` | `prover` (indexed), `beliefHash` (indexed), `zkVerified`, `sourceChainId` |
| `TrustMessageSent` | On `_sendTrustMessage` | `nonce` (indexed), `messageType` (indexed), `messageHash`, `destinationChainId` |
| `TrustMessageReceived` | On `_processTrustMessage` | `nonce` (indexed), `messageType` (indexed), `messageHash`, `sourceChainId` |

### 10.9 Security Invariants

1. **Tier expiry:** `isTrustTierRecognized` enforces expiry. Stale tier attestations automatically stop being recognized after `expiresAt`.
2. **VNS expiry:** `resolveVNS` and `reverseResolveVNS` both enforce expiry on VNS records.
3. **No self-attestation:** Only `authorizedRelayers` can push trust data. An agent cannot attest their own tier.
4. **ZK attestation immutability (partial):** Once a `(beliefHash, prover)` entry is stored, it can be overwritten by a subsequent relay — the contract does not lock attestations. This is intentional to allow updates if a belief is re-attested.

---

## 11. MockTeleporterMessenger.sol

**File:** `theloopbreaker-site/contracts/teleporter/mocks/MockTeleporterMessenger.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/contracts/teleporter/mocks/MockTeleporterMessenger.sol`  
**Category:** Mock teleporter (test utility)  
**Compiler:** `^0.8.25`  
**SHA-256:** `c1e9e303c17157b3c128649e062cdf71aef3839eff706befa162c308e024b707`

### 11.1 Purpose

Test mock that simulates `TeleporterMessenger` for unit testing bridge contracts without running a full ICM stack. Implements `ITeleporterMessenger`. Records all sent messages in a public array and provides a `deliverMessage` helper that calls the destination receiver's `receiveTeleporterMessage` — enabling end-to-end round-trip testing in a single test environment.

### 11.2 Full Source

```solidity
  1	// SPDX-License-Identifier: MIT
  2	pragma solidity ^0.8.25;
  3	import "../ITeleporterMessenger.sol";
  4	import "../ITeleporterReceiver.sol";
  5	
  6	contract MockTeleporterMessenger is ITeleporterMessenger {
  7	    struct SentMessage {
  8	        bytes32 destinationBlockchainID;
  9	        address destinationAddress;
  10	        uint256 requiredGasLimit;
  11	        bytes message;
  12	    }
  13	
  14	    SentMessage[] public sentMessages;
  15	    uint256 private _nonce;
  16	    bytes32 public blockchainID;
  17	
  18	    constructor(bytes32 _blockchainID) { blockchainID = _blockchainID; }
  19	
  20	    function sendCrossChainMessage(TeleporterMessageInput calldata messageInput)
  21	        external override returns (bytes32)
  22	    {
  23	        _nonce++;
  24	        sentMessages.push(SentMessage({
  25	            destinationBlockchainID: messageInput.destinationBlockchainID,
  26	            destinationAddress:      messageInput.destinationAddress,
  27	            requiredGasLimit:        messageInput.requiredGasLimit,
  28	            message:                 messageInput.message
  29	        }));
  30	        bytes32 messageID = keccak256(abi.encodePacked(blockchainID, _nonce));
  31	        // Emit the full SendCrossChainMessage event (per interface)
  32	        // [emit omitted in condensed view — full in source]
  33	        return messageID;
  34	    }
  35	
  36	    // All other ITeleporterMessenger functions revert with "not implemented"
  37	    // except: messageReceived → false, getMessageHash → bytes32(0), getNextMessageID → bytes32(0)
  38	
  39	    function deliverMessage(
  40	        address receiver,
  41	        bytes32 sourceBlockchainID,
  42	        address originSenderAddress,
  43	        bytes calldata message
  44	    ) external {
  45	        ITeleporterReceiver(receiver).receiveTeleporterMessage(
  46	            sourceBlockchainID, originSenderAddress, message
  47	        );
  48	    }
  49	
  50	    function getSentMessageCount() external view returns (uint256) { ... }
  51	    function getLastSentMessage() external view returns (SentMessage memory) { ... }
  52	}
```

*(Full source: `/home/user/workspace/theloopbreaker-site/contracts/teleporter/mocks/MockTeleporterMessenger.sol`, 126 lines)*

### 11.3 Key Behavior

**`sendCrossChainMessage`:** Increments `_nonce`, pushes a condensed `SentMessage` to `sentMessages`, derives `messageID = keccak256(abi.encodePacked(blockchainID, _nonce))`, emits `SendCrossChainMessage`. Does NOT call the destination chain (cross-chain delivery must be triggered manually via `deliverMessage`).

**`deliverMessage`:** The test equivalent of the ICM relayer. Calls `ITeleporterReceiver(receiver).receiveTeleporterMessage(sourceBlockchainID, originSenderAddress, message)` directly. Callers must ensure `receiver`'s `onlyTeleporter` modifier will pass (i.e. set `teleporterMessenger = address(this mock)` on the bridge under test).

**Unimplemented functions:** `retrySendCrossChainMessage`, `addFeeAmount`, `receiveCrossChainMessage`, `retryMessageExecution` all `revert("MockTeleporter: not implemented")`. `messageReceived` returns `false`; `getMessageHash` and `getNextMessageID` return `bytes32(0)`.

**Constructor:** Takes a `bytes32 _blockchainID` to identify the source chain in message ID derivation — tests typically pass a meaningful blockchain ID constant (e.g. `keccak256("base")`).

---

## 12. VaultfireNameService.sol

**File:** `vaultfire-vns/contracts/VaultfireNameService.sol`  
**Path:** `/home/user/workspace/vaultfire-vns/contracts/VaultfireNameService.sol`  
**Category:** VaultfireNameService VNS  
**Compiler:** `^0.8.25`  
**SHA-256:** `5f28903e2a72cbcb1cb9b3f4c68a23709abc8dad0ea9ea40cd43c997e2ab70b4`

### 12.1 Purpose

`VaultfireNameService` is a fully permissionless, fully on-chain ENS-style name registry for the Vaultfire ecosystem. It maps human-readable `.vaultfire` names (e.g. `alice.vaultfire`) to Ethereum addresses. Unlike ENS, there is no auction, no annual fee, and no admin — users pay only gas. The contract supports forward resolution (name → address), reverse resolution (address → name), name transfer, and name release.

### 12.2 Full Source Code

```solidity
  1	// SPDX-License-Identifier: MIT
  2	pragma solidity ^0.8.25;
  3	
  4	contract VaultfireNameService {
  5	
  6	    struct NameRecord {
  7	        address owner;
  8	        uint256 registeredAt;
  9	        bool exists;
  10	    }
  11	
  12	    mapping(bytes32 => NameRecord) private _names;        // nameHash => record
  13	    mapping(address => string)    private _reverseNames;  // address => name string
  14	    mapping(address => bool)      private _hasName;        // address => has registered name
  15	    uint256 public totalNames;
  16	
  17	    event NameRegistered(string indexed nameIndexed, string name, address indexed owner, uint256 timestamp);
  18	    event NameTransferred(string indexed nameIndexed, string name,
  19	        address indexed previousOwner, address indexed newOwner, uint256 timestamp);
  20	    event NameReleased(string indexed nameIndexed, string name, address indexed previousOwner, uint256 timestamp);
  21	
  22	    function registerName(string calldata name) external {
  23	        require(!_hasName[msg.sender], "Address already has a name");
  24	        bytes memory nameBytes = bytes(name);
  25	        require(nameBytes.length >= 3 && nameBytes.length <= 32, "Name must be 3-32 characters");
  26	        for (uint256 i = 0; i < nameBytes.length; i++) {
  27	            bytes1 c = nameBytes[i];
  28	            bool isLower = (c >= 0x61 && c <= 0x7A);
  29	            bool isDigit = (c >= 0x30 && c <= 0x39);
  30	            bool isHyphen = (c == 0x2D);
  31	            require(isLower || isDigit || isHyphen, "Invalid character");
  32	            if (i == 0 || i == nameBytes.length - 1) {
  33	                require(!isHyphen, "Cannot start or end with hyphen");
  34	            }
  35	        }
  35	        bytes32 nameHash = keccak256(nameBytes);
  36	        require(!_names[nameHash].exists, "Name already taken");
  37	        _names[nameHash] = NameRecord({owner: msg.sender, registeredAt: block.timestamp, exists: true});
  38	        _reverseNames[msg.sender] = name;
  39	        _hasName[msg.sender] = true;
  40	        totalNames++;
  41	        emit NameRegistered(name, name, msg.sender, block.timestamp);
  42	    }
  43	
  44	    function transferName(address newOwner) external {
  45	        require(_hasName[msg.sender], "No name to transfer");
  46	        require(newOwner != address(0), "Cannot transfer to zero address");
  47	        require(newOwner != msg.sender, "Cannot transfer to self");
  48	        require(!_hasName[newOwner], "New owner already has a name");
  49	        string memory name = _reverseNames[msg.sender];
  50	        bytes32 nameHash = keccak256(bytes(name));
  51	        _names[nameHash].owner = newOwner;
  52	        _reverseNames[newOwner] = name;
  53	        _hasName[newOwner] = true;
  54	        delete _reverseNames[msg.sender];
  55	        _hasName[msg.sender] = false;
  56	        emit NameTransferred(name, name, msg.sender, newOwner, block.timestamp);
  57	    }
  58	
  59	    function releaseName() external {
  60	        require(_hasName[msg.sender], "No name to release");
  61	        string memory name = _reverseNames[msg.sender];
  62	        bytes32 nameHash = keccak256(bytes(name));
  63	        delete _names[nameHash];
  64	        delete _reverseNames[msg.sender];
  65	        _hasName[msg.sender] = false;
  66	        totalNames--;
  67	        emit NameReleased(name, name, msg.sender, block.timestamp);
  68	    }
  69	
  70	    function resolveNameToAddress(string calldata name)
  71	        external view returns (address owner, uint256 registeredAt, bool exists) { ... }
  72	    function resolveAddressToName(address addr)
  73	        external view returns (string memory name, bool hasRegistered) { ... }
  74	    function isNameAvailable(string calldata name) external view returns (bool available) { ... }
  75	    function hasRegisteredName(address addr) external view returns (bool) { ... }
  76	}
```

*(Full source: 207 lines — view function bodies abbreviated; all read from private mappings)*

### 12.3 Line-by-Line Walkthrough

| Lines | What happens |
|-------|-------------|
| 22 | `registerName(string calldata name)` — one-name-per-address constraint checked first. |
| 24–25 | Name length check: 3–32 bytes. This is a byte-length check, not character-length — ASCII-only names are fine; multi-byte UTF-8 names would fail character validation anyway. |
| 26–34 | Character-by-character validation loop. Accepted: `a-z` (0x61–0x7A), `0-9` (0x30–0x39), `-` (0x2D). Hyphens disallowed at position 0 or `length-1`. No uppercase, no dots, no underscores. |
| 35–36 | `nameHash = keccak256(nameBytes)`. Collision check against `_names[nameHash].exists`. |
| 37–41 | Write: `_names[nameHash]`, `_reverseNames[msg.sender]`, `_hasName[msg.sender]`, increment `totalNames`. Emit `NameRegistered`. |
| 44–57 | `transferName(newOwner)` — transfers the caller's name. Guards: caller has a name, `newOwner != address(0)`, `newOwner != msg.sender`, `newOwner` does not already have a name. Updates `_names[nameHash].owner`, `_reverseNames`, `_hasName` on both sides. Emits `NameTransferred`. |
| 59–68 | `releaseName()` — deletes `_names[nameHash]` and `_reverseNames[msg.sender]`, clears `_hasName`, decrements `totalNames`. Emits `NameReleased`. The name is now available for registration. |
| 70–78 | View functions — straightforward reads from private mappings. `resolveNameToAddress` returns all three `NameRecord` fields. `resolveAddressToName` returns the name string and whether the address has registered. |

### 12.4 State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|-----------|---------|
| `_names` | `mapping(bytes32 => NameRecord)` | mutable, private | Primary registry: name hash → record. |
| `_reverseNames` | `mapping(address => string)` | mutable, private | Reverse registry: address → name string. Deleted on transfer/release. |
| `_hasName` | `mapping(address => bool)` | mutable, private | One-name-per-address guard. Faster than checking `bytes(_reverseNames[addr]).length`. |
| `totalNames` | `uint256` | mutable, public | Running count of active (non-released) registrations. Decremented on `releaseName`. |

### 12.5 Functions — Full Behavioral Specification

**`registerName(string name)`**  
External. Validates one-per-address, length 3–32, character set, uniqueness. Stores forward and reverse mappings. Increments `totalNames`. Emits `NameRegistered` with both indexed (`nameIndexed`) and non-indexed (`name`) fields — the indexed field supports event filter by name, but note that Solidity hashes indexed `string` values, so the raw string can only be retrieved from the non-indexed field.

**`transferName(address newOwner)`**  
External. Transfers the caller's name to `newOwner`. Fails if `newOwner` already has a name (one-per-address invariant is preserved). Clears the sender's reverse mapping; sets the new owner's. Does not change `totalNames`.

**`releaseName()`**  
External. Deletes the forward record and the caller's reverse mapping. Decrements `totalNames`. The name becomes available for re-registration.

**`resolveNameToAddress(string name) → (address owner, uint256 registeredAt, bool exists)`**  
External `view`. Hashes the input, reads `_names[hash]`, returns all fields. Returns zero-value `NameRecord` (owner = `address(0)`, exists = `false`) for unregistered names.

**`resolveAddressToName(address addr) → (string name, bool hasRegistered)`**  
External `view`. Reads `_reverseNames[addr]` and `_hasName[addr]`.

**`isNameAvailable(string name) → bool`**  
External `view`. Returns `!_names[keccak256(bytes(name))].exists`.

**`hasRegisteredName(address addr) → bool`**  
External `view`. Returns `_hasName[addr]`.

### 12.6 VNS Fee Structure

**There are no fees.** Registration, transfer, and release are completely free — callers pay only gas. This is a deliberate design choice for the Vaultfire ecosystem where agents (especially AI agents) should have friction-free identity registration.

The `@custom:security` tag reads "No admin functions — fully permissionless" and `@custom:ethics` reads "Privacy-first: no KYC, wallet addresses only."

### 12.7 Events

| Event | When fired | Indexed fields |
|-------|-----------|---------------|
| `NameRegistered` | On successful `registerName` | `nameIndexed` (string hash), `owner`. Non-indexed `name` carries the raw string. |
| `NameTransferred` | On successful `transferName` | `nameIndexed`, `previousOwner`, `newOwner`. |
| `NameReleased` | On `releaseName` | `nameIndexed`, `previousOwner`. |

Note: Solidity's indexed `string` fields are stored as `keccak256(string)` in the log topic. Filtering by name hash is efficient; recovering the actual string requires the non-indexed copy.

### 12.8 Security Invariants

1. **One name per address:** `_hasName[msg.sender]` checked before registration and before accepting a transfer. No address can hold two names simultaneously.
2. **No name squatting:** Names are assigned to addresses, not to people. Any address can claim any available name — there is no admin reservation or namespace partitioning.
3. **No admin:** No owner, no upgradeability, no fee recipient. The contract cannot be altered after deployment.
4. **No ETH:** No `receive()`, no `payable`. Entirely gas-only.
5. **Hash collision risk:** Theoretically, two different names could have the same `keccak256` hash — practically impossible (256-bit hash). The `exists` check guards against double-registration at the same hash.

### 12.9 Standards Compliance

VNS does not implement ENS (EIP-137) — it is a simpler, protocol-specific registry without resolver contracts, TTL, or node hierarchies. It does not implement ERC-721 (names are not NFTs and cannot be traded on NFT marketplaces). The TrustPortabilityExtension syncs VNS bindings cross-chain via Teleporter.

### 12.10 Dependencies + Rebuild Recipe

**No external imports.** Pure Solidity.

```bash
cd vaultfire-vns
# Hardhat:
npx hardhat compile
# Or Foundry:
forge build
```

**Constructor:** No constructor arguments — the contract deploys and is immediately operational.

---

## 13. VaultfireStreamNFT.sol

**File:** `theloopbreaker-site/reward_engine/contracts/VaultfireStreamNFT.sol`  
**Path:** `/home/user/workspace/theloopbreaker-site/reward_engine/contracts/VaultfireStreamNFT.sol`  
**Category:** VaultfireStreamNFT — reward streaming  
**Compiler:** `^0.8.20`  
**SHA-256:** `2bf833f7633fa18e7d8cbe857eba8d52a2ad937b7cd05d688b57107839bc6f9a`

### 13.1 Purpose

`VaultfireStreamNFT` is an ERC-721 contract where each token represents a **reward stream** — a schedule of linearly-unlocking reward points or multiplier units assigned to a contributor address. The stream model is used by the Vaultfire reward engine to grant contributors time-vested points: e.g. "you earned 1000 multiplier units that unlock linearly over 30 days." The NFT is non-transferable (soulbound behavior via a `_update` override that reverts on transfers) but can be burned by the stream manager.

### 13.2 Full Source Code

```solidity
  1	// SPDX-License-Identifier: MIT
  2	pragma solidity ^0.8.20;
  3	import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
  4	import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
  5	
  6	contract VaultfireStreamNFT is ERC721URIStorage, AccessControl {
  7	    bytes32 public constant STREAM_MANAGER_ROLE = keccak256("STREAM_MANAGER_ROLE");
  8	
  9	    struct Stream {
  10	        address recipient;
  11	        uint64 startTime;
  12	        uint64 endTime;
  13	        uint256 totalAmount;    // micro-units of the multiplier or reward points
  14	        uint256 claimedAmount;
  15	        bytes32 userId;
  16	    }
  17	
  18	    uint256 private _nextTokenId = 1;
  19	    mapping(uint256 => Stream) private _streams;
  20	    mapping(address => uint256) public latestStreamByRecipient;
  21	
  22	    event StreamStarted(uint256 indexed tokenId, address indexed recipient,
  23	        bytes32 indexed userId, uint64 startTime, uint64 endTime,
  24	        uint256 totalAmount, string metadataURI);
  25	    event StreamClaimed(uint256 indexed tokenId, address indexed claimant,
  26	        uint256 unlockedAmount, uint256 totalClaimed);
  27	    event StreamCancelled(uint256 indexed tokenId, address indexed operator, uint64 cancelledAt);
  28	
  29	    constructor(address admin) ERC721("Vaultfire Reward Stream", "VFSTREAM") {
  30	        if (admin == address(0)) admin = msg.sender;
  31	        _grantRole(DEFAULT_ADMIN_ROLE, admin);
  32	        _grantRole(STREAM_MANAGER_ROLE, admin);
  33	    }
  34	
  35	    function startStream(address recipient, uint256 totalAmount, uint64 startTime,
  36	        uint64 endTime, string calldata metadataURI, bytes32 userId)
  37	        external onlyRole(STREAM_MANAGER_ROLE) returns (uint256 tokenId)
  38	    {
  39	        require(recipient != address(0), "recipient required");
  40	        require(totalAmount > 0, "amount required");
  41	        uint64 actualStart = startTime == 0 ? uint64(block.timestamp) : startTime;
  42	        require(endTime > actualStart, "invalid duration");
  43	        tokenId = _nextTokenId++;
  44	        _streams[tokenId] = Stream{recipient, actualStart, endTime, totalAmount, 0, userId};
  45	        _safeMint(recipient, tokenId);
  46	        if (bytes(metadataURI).length > 0) _setTokenURI(tokenId, metadataURI);
  47	        latestStreamByRecipient[recipient] = tokenId;
  48	        emit StreamStarted(tokenId, recipient, userId, actualStart, endTime, totalAmount, metadataURI);
  49	    }
  50	
  51	    function claimable(uint256 tokenId) public view returns (uint256) {
  52	        Stream memory stream = _streams[tokenId];
  53	        if (stream.recipient == address(0)) return 0;
  54	        if (block.timestamp <= stream.startTime) return 0;
  55	        uint64 effectiveTime = block.timestamp < stream.endTime
  56	            ? uint64(block.timestamp) : stream.endTime;
  57	        uint64 elapsed = effectiveTime - stream.startTime;
  58	        uint64 duration = stream.endTime - stream.startTime;
  59	        if (duration == 0) return 0;
  60	        uint256 unlocked = (stream.totalAmount * elapsed) / duration;
  61	        if (unlocked <= stream.claimedAmount) return 0;
  62	        return unlocked - stream.claimedAmount;
  63	    }
  64	
  65	    function claim(uint256 tokenId) external returns (uint256 unlockedAmount) {
  66	        Stream storage stream = _streams[tokenId];
  67	        require(stream.recipient != address(0), "stream missing");
  68	        require(_isApprovedOrOwner(msg.sender, tokenId), "not authorized");
  69	        unlockedAmount = claimable(tokenId);
  70	        stream.claimedAmount += unlockedAmount;
  71	        emit StreamClaimed(tokenId, msg.sender, unlockedAmount, stream.claimedAmount);
  72	    }
  73	
  74	    function cancelStream(uint256 tokenId, bool burnToken)
  75	        external onlyRole(STREAM_MANAGER_ROLE)
  76	    {
  77	        Stream storage stream = _streams[tokenId];
  78	        require(stream.recipient != address(0), "stream missing");
  79	        stream.endTime = uint64(block.timestamp);
  80	        emit StreamCancelled(tokenId, msg.sender, stream.endTime);
  81	        if (burnToken) _burn(tokenId);
  82	    }
  83	
  84	    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
  85	        address from = _ownerOf(tokenId);
  86	        if (from != address(0) && to != address(0)) revert("transfers disabled");
  87	        address previousOwner = super._update(to, tokenId, auth);
  88	        if (to == address(0)) delete latestStreamByRecipient[previousOwner];
  89	        else latestStreamByRecipient[to] = tokenId;
  90	        return previousOwner;
  91	    }
  92	
  93	    function _burn(uint256 tokenId) internal override {
  94	        super._burn(tokenId);
  95	        delete _streams[tokenId];
  96	    }
  97	}
```

*(Full source: 209 lines — `streamDetails`, `activeMultiplier`, `nextTokenId`, `supportsInterface` abbreviated)*

### 13.3 Line-by-Line Walkthrough

| Lines | What happens |
|-------|-------------|
| 3–4 | Imports OpenZeppelin's `AccessControl` (role-based auth) and `ERC721URIStorage` (token URI per-token storage). |
| 7 | `STREAM_MANAGER_ROLE = keccak256("STREAM_MANAGER_ROLE")` — role constant for automation accounts. |
| 9–16 | `Stream` struct: packed `uint64` for timestamps (saves storage vs `uint256`); `totalAmount` and `claimedAmount` in `uint256`; `userId` as `bytes32` for off-chain user correlation. |
| 18 | `_nextTokenId = 1` — token IDs start at 1, not 0. This avoids ambiguity with uninitialized `uint256` reads. |
| 20 | `latestStreamByRecipient` — tracks the most recent active token per recipient. Updated on mint and cleared on burn. |
| 29–33 | Constructor: `admin = msg.sender` if `address(0)` passed (fallback). Grants both `DEFAULT_ADMIN_ROLE` and `STREAM_MANAGER_ROLE` to admin. |
| 35–49 | `startStream` — mints a new stream NFT. If `startTime = 0`, uses `block.timestamp`. Requires `endTime > actualStart`. Stores the `Stream` struct, mints via `_safeMint`, optionally sets URI, updates `latestStreamByRecipient`. |
| 51–63 | `claimable` — view function computing linearly unlocked-minus-claimed amount. See §13.6 for math. Returns 0 for nonexistent streams, before start, and after full claim. |
| 65–72 | `claim` — writes `stream.claimedAmount += unlockedAmount`. Uses `_isApprovedOrOwner` per ERC-721 standard — since transfers are disabled, this effectively checks token ownership. |
| 74–82 | `cancelStream` — sets `endTime = block.timestamp` (freezes future unlock at current time), emitting `StreamCancelled`. If `burnToken = true`, calls `_burn`. |
| 84–91 | `_update` override — the non-transferability hook. If `from != address(0)` (not a mint) and `to != address(0)` (not a burn), reverts with `"transfers disabled"`. This is the OpenZeppelin v5 override pattern (replaces the `_beforeTokenTransfer` hook from v4). |
| 93–96 | `_burn` override — calls `super._burn` and deletes the `_streams[tokenId]` entry (prevents ghost stream data). |
| 159–178 | `activeMultiplier(account)` — returns the multiplier value of the latest stream. `multiplierMicros = stream.totalAmount` (the total amount stored is interpreted as the multiplier in micro-units). `active = block.timestamp < stream.endTime && claimedAmount < totalAmount`. |
| 181–188 | `supportsInterface` — delegates to both `ERC721URIStorage` and `AccessControl` via the `super` chain. |

### 13.4 State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|-----------|---------|
| `STREAM_MANAGER_ROLE` | `bytes32 constant` | immutable | Role hash for automation accounts authorized to start/cancel streams. |
| `_nextTokenId` | `uint256` | mutable, private | Auto-incrementing token ID counter starting at 1. |
| `_streams` | `mapping(uint256 => Stream)` | mutable, private | Stream data per token ID. Deleted on burn. |
| `latestStreamByRecipient` | `mapping(address => uint256)` | mutable, public | Tracks the most recent token ID for each recipient. Updated on mint and cleared on burn. |

### 13.5 Functions — Full Behavioral Specification

**`nextTokenId() → uint256`**  
External `view`. Returns `_nextTokenId` — the ID that will be assigned to the next minted stream.

**`startStream(recipient, totalAmount, startTime, endTime, metadataURI, userId) onlyRole(STREAM_MANAGER_ROLE) → uint256 tokenId`**  
External. Creates a new stream and mints the NFT. If `startTime = 0`, uses `block.timestamp`. Reverts if `totalAmount = 0` or `endTime <= actualStart`. Returns the new token ID.

**`claimable(uint256 tokenId) → uint256`**  
Public `view`. Returns the amount of reward units currently available to claim (unlocked minus already claimed). See §13.6 for the math. Returns 0 for nonexistent streams.

**`claim(uint256 tokenId) → uint256 unlockedAmount`**  
External. Accrues the claimable amount into `stream.claimedAmount`. Requires `_isApprovedOrOwner(msg.sender, tokenId)`. Returns the amount claimed in this call. Does not transfer any ERC-20 tokens — this is a pure accounting function; the integration layer consumes `claimedAmount` to unlock real rewards.

**`cancelStream(uint256 tokenId, bool burnToken) onlyRole(STREAM_MANAGER_ROLE)`**  
External. Sets `endTime = now`, halting future unlock accumulation. Optionally burns the NFT (and deletes stream data).

**`streamDetails(uint256 tokenId) → (recipient, startTime, endTime, totalAmount, claimedAmount, userId, metadataURI, claimableAmount)`**  
External `view`. Aggregates all stream data plus live `claimable(tokenId)` value in a single call.

**`activeMultiplier(address account) → (uint256 multiplierMicros, uint256 tokenId, bool active)`**  
External `view`. Returns the latest stream's `totalAmount` as the multiplier, with an `active` boolean indicating whether the stream is still ongoing and not fully claimed.

**`supportsInterface(bytes4 interfaceId)`**  
Public `view override`. Delegates to both `ERC721URIStorage` and `AccessControl` super chains.

**`_update(address to, uint256 tokenId, address auth) internal override → address`**  
Internal override of the OZ v5 token update hook. Blocks all transfers (mint → burn path passes, burn → zero path passes, but `from != 0 && to != 0` reverts).

**`_burn(uint256 tokenId) internal override`**  
Internal. Calls `super._burn` then deletes `_streams[tokenId]`.

### 13.6 Math — Streaming Reward Formula

The linear unlock formula used in `claimable`:

\[
\text{unlocked}(t) = \text{totalAmount} \times \frac{\min(t, t_{\text{end}}) - t_{\text{start}}}{t_{\text{end}} - t_{\text{start}}}
\]

\[
\text{claimable}(t) = \max(0,\ \text{unlocked}(t) - \text{claimedAmount})
\]

In Solidity (lines 101–112):

```solidity
uint64 effectiveTime = block.timestamp < stream.endTime
    ? uint64(block.timestamp) : stream.endTime;
uint64 elapsed  = effectiveTime - stream.startTime;
uint64 duration = stream.endTime - stream.startTime;
uint256 unlocked = (stream.totalAmount * elapsed) / duration;
return (unlocked > stream.claimedAmount) ? unlocked - stream.claimedAmount : 0;
```

**Properties:**
- Linear: at any time `t` within the stream period, unlock is proportional to elapsed time fraction.
- Fully unlocked at `t_end`: `effectiveTime` is clamped to `endTime`, so after the stream ends the full `totalAmount` is unlocked.
- Integer division truncation: the division `(totalAmount * elapsed) / duration` may truncate — the recipient loses at most 1 unit per claim due to rounding. This is acceptable for reward-point accounting.
- Re-entrant claim safety: `claimedAmount` is incremented atomically in `claim`; a re-entrant call would see the updated `claimedAmount` and return 0.

**Example:** `totalAmount = 1000`, `startTime = day 0`, `endTime = day 30`, claim at day 10:
\[
\text{unlocked} = 1000 \times \frac{10}{30} = 333\ \text{(truncated)}
\]
\[
\text{claimable} = 333 - 0 = 333
\]

After claiming, `claimedAmount = 333`. Claim again at day 20:
\[
\text{unlocked} = 1000 \times \frac{20}{30} = 666,\quad \text{claimable} = 666 - 333 = 333
\]

### 13.7 Events

| Event | When fired | Fields |
|-------|-----------|--------|
| `StreamStarted` | On `startStream` | `tokenId` (indexed), `recipient` (indexed), `userId` (indexed), `startTime`, `endTime`, `totalAmount`, `metadataURI` |
| `StreamClaimed` | On `claim` | `tokenId` (indexed), `claimant` (indexed), `unlockedAmount` (this call), `totalClaimed` (cumulative) |
| `StreamCancelled` | On `cancelStream` | `tokenId` (indexed), `operator` (indexed), `cancelledAt` |

### 13.8 Security Invariants

1. **Non-transferable:** `_update` revert ensures streams cannot be sold or moved. Each stream is permanently bound to its recipient address.
2. **Role-gated creation/cancellation:** Only `STREAM_MANAGER_ROLE` holders can start or cancel streams. Default: the admin set at construction.
3. **Claim idempotency:** `claimable()` returns 0 if `unlocked <= claimedAmount`. Repeated claims return 0 and emit `StreamClaimed(0)` — harmless but wasteful.
4. **No ETH:** Contract holds no ETH; `totalAmount` and `claimedAmount` are unitless accounting integers interpreted by the off-chain reward engine.
5. **Burn cleanup:** `_burn` deletes `_streams[tokenId]` — prevents ghost data from occupying storage after a stream is terminated.
6. **`latestStreamByRecipient` freshness:** Tracks only the most recently minted token per recipient. A recipient can have multiple streams if earlier ones were not burned — `latestStreamByRecipient` will point to the last one and `activeMultiplier` will only check that one. The off-chain reward engine should iterate past streams independently if needed.

### 13.9 Standards Compliance

- **ERC-721** (EIP-721): Inherits from `ERC721URIStorage`. Standard `balanceOf`, `ownerOf`, `tokenURI` are available. `approve`, `setApprovalForAll`, `safeTransferFrom`, `transferFrom` are inherited but all revert via the `_update` hook.
- **ERC-721URIStorage** (OZ extension): Per-token URI stored in `_tokenURIs` mapping. `tokenURI` returns the stored URI if set, otherwise falls back to base URI (empty in this contract).
- **AccessControl** (OZ): `DEFAULT_ADMIN_ROLE` for role management, `STREAM_MANAGER_ROLE` for automation. Unlike Ownable, multiple addresses can hold the manager role simultaneously.

### 13.10 Dependencies + Rebuild Recipe

**Dependencies:**
- `@openzeppelin/contracts ^5.x` (for `AccessControl`, `ERC721URIStorage`, OZ v5 `_update` hook)

```bash
cd theloopbreaker-site/reward_engine
npm install @openzeppelin/contracts
npx hardhat compile
```

Or:
```bash
forge install OpenZeppelin/openzeppelin-contracts
forge build
```

**Constructor arguments:**
- `admin`: Address granted `DEFAULT_ADMIN_ROLE` and `STREAM_MANAGER_ROLE`. Defaults to `msg.sender` if `address(0)` passed.

---

## Appendix A — Contract Summary Table

| # | File | Category | Lines | Compiler | Key Standard |
|---|------|----------|-------|----------|-------------|
| 1 | `VaultfireStreetCred.sol` | Sprint-5192 ERC-5192 | 449 | 0.8.24 | ERC-5192, ERC-721 |
| 2 | `VaultfireStreetCred.t.sol` | Sprint-5192 Test | 265 | 0.8.24 | Foundry fork test |
| 3 | `VaultfireBatchOnboarder.sol` | Sprint-7702 EIP-7702 | 281 | ^0.8.24 | EIP-7702 |
| 4 | `VaultfireBatchOnboarder.t.sol` | Sprint-7702 Test | 417 | ^0.8.24 | Foundry fork test |
| 5 | `Deploy.s.sol` | Sprint-7702 Deploy | 41 | ^0.8.24 | Foundry Script |
| 6 | `ITeleporterMessenger.sol` | Teleporter Interface | 92 | ^0.8.25 | Avalanche ICM |
| 7 | `ITeleporterReceiver.sol` | Teleporter Interface | 23 | ^0.8.25 | Avalanche ICM |
| 8 | `VaultfireTeleporterBridge.sol` | TeleporterBridge V1 | 692 | ^0.8.25 | ITeleporterReceiver |
| 9 | `VaultfireTeleporterBridgeV3.sol` | TeleporterBridge V3.3 | 903 | ^0.8.25 | ITeleporterReceiver |
| 10 | `TrustPortabilityExtension.sol` | TrustPortability | 706 | ^0.8.25 | ITeleporterReceiver |
| 11 | `MockTeleporterMessenger.sol` | Mock Teleporter | 126 | ^0.8.25 | ITeleporterMessenger |
| 12 | `VaultfireNameService.sol` | VNS | 207 | ^0.8.25 | — (custom registry) |
| 13 | `VaultfireStreamNFT.sol` | StreamNFT | 209 | ^0.8.20 | ERC-721, AccessControl |

---

## Appendix B — Cross-Contract Address Registry

All Vaultfire contracts on Base mainnet (`chain ID 8453`):

| Contract | Address |
|----------|---------|
| ERC8004IdentityRegistry | `0xa7BD20bf5De63df949cA5Be2F20835978eCba81A` |
| AIPartnershipBondsV2 | `0x01C479F0c039fEC40c0Cf1c5C921bab457d57441` |

These addresses are hardcoded as `public constant` in `VaultfireBatchOnboarder` and used as constructor arguments in `VaultfireStreetCred`.

---

## Appendix C — EIP-7702 Delegation Protocol Reference

EIP-7702 (finalized in Ethereum Pectra, EIP-7702) defines a new transaction type (Type 4) that includes an `authorization_list`: a list of `(chain_id, address, nonce)` tuples signed by EOA private keys. Upon execution:

1. For each authorized tuple, if the signing key matches the EOA, the EVM writes `0xEF0100 || address` to the EOA's code field (the "delegation designator" prefix).
2. Calls to the EOA now delegate-call the specified address's bytecode, with `address(this) = EOA`, `msg.sender = caller`, storage scope = EOA.
3. The delegation persists across subsequent transactions.
4. Revocation: sign an authorization tuple to `address(0)`. This clears the delegation designator.

**Security implications for `VaultfireBatchOnboarder`:**
- Delegation is persistent, not one-shot. All future calls to the EOA route through the delegate — hence the explicit absence of `fallback()` and `receive()`.
- Cross-chain replay: A `chain_id = 0` authorization replays on all EVM chains. `block.chainid == BASE_CHAIN_ID` mitigates this at the execution layer.
- Stateless delegate pattern prevents `initialize()` frontrunning.

---

## Appendix D — ERC-5192 Soulbound Standard Reference

ERC-5192 (Minimal Soulbound NFT interface) specifies:
- A `locked(uint256 tokenId)` view function — returns `true` if the token is non-transferable.
- A `Locked(uint256 tokenId)` event — must be emitted when a token becomes locked.
- An `Unlocked(uint256 tokenId)` event — must be emitted when a token becomes unlocked.

`VaultfireStreetCred` satisfies this by: always returning `true` from `locked()`; emitting `Locked(tokenId)` at mint; never emitting `Unlocked`. The interface ID `0xb45a3c0e` is advertised via `supportsInterface`.

---

## Appendix E — Teleporter Blockchain IDs

Avalanche Teleporter identifies chains via `bytes32` blockchain IDs, not EVM chain IDs. The mappings relevant to Vaultfire:

| Chain | EVM Chain ID | Teleporter bytes32 ID |
|-------|-------------|----------------------|
| Base mainnet | 8453 | `keccak256(abi.encodePacked(8453))` (for non-Avalanche chains) |
| Avalanche C-Chain | 43114 | `0x7fc93d85c6d62be589069a9ca34c1b4d9d11d3b82a4e4e456b7a4a3b5a4b3c2d` (official; use `ava-labs/icm-contracts` constants) |

The bridge's `setRemoteBridge` accepts an arbitrary `bytes32` and `uint256` pair, so the exact Teleporter blockchain ID must be configured post-deployment using the chain's official value from the `ava-labs/icm-contracts` repository.
