# Vaultfire Codex

**Complete line-by-line technical reference for every Solidity contract in the Vaultfire system.**

Generated: 2026-05-04 UTC
Builder: ghostkey.base.eth (`0xfA15Ee28939B222B0448261A22156070f0A7813C`)
License: MIT
No token. No VC. Solo-built.

---

## Purpose

This document is a complete rebuild blueprint. If every other file in the world disappeared, the contents of this Codex are sufficient — line-by-line — to reconstruct the entire Vaultfire smart contract system from zero.

It documents **209 Solidity files** across the workspace, deduplicated to **177 unique contracts** by SHA256 content hash, organized into 8 logical batches.

---

## Live on Base mainnet (chain 8453)

| Contract | Address | Status |
|---|---|---|
| **Vaultfire Genesis NFT** (1/1) | [`0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C`](https://basescan.org/address/0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C) | Token #1 minted to ghostkey |
| **Vaultfire Founders Edition** (5 max) | [`0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE`](https://basescan.org/address/0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE) | Open for public mint at 0.1 ETH |

Both deployed via Arachnid CREATE2 factory `0x4e59b44847b379578588920ca78fbf26c0b4956c`.

Compiler: solc 0.8.25, optimizer 200 runs, via-ir, evm cancun.

CREATE2 salts:
- Genesis: `0x7661756c74666972652d67656e657369732d3230323600000000000000000000` (= "vaultfire-genesis-2026")
- Founders: `0x7661756c74666972652d666f756e646572732d32303236000000000000000000` (= "vaultfire-founders-2026")

---

## Index

| # | Document | Contracts | Size | What it covers |
|---|---|---|---|---|
| 01 | [`01_LIVE_NFT.md`](./01_LIVE_NFT.md) | 3 | 83 KB · 1,724 lines | Live mainnet NFTs — Genesis 1/1, Founders Edition (5 max), deploy script. Full math for royalty (EIP-2981), price wei↔ETH, CREATE2 derivation, complete Foundry rebuild recipe. |
| 02 | [`02_V3_BONDS.md`](./02_V3_BONDS.md) | 25 | 132 KB · 2,349 lines | Audited V3 economic core — AIAccountabilityBonds, AIPartnershipBonds, BaseDignityBond, BaseYieldPoolBond, BondInsurancePool, BIPVault, PartnershipBondsEnhanced. All version snapshots (V2/V3.1/V3.3) with audit-finding cross-reference. |
| 03 | [`03_ERC8004.md`](./03_ERC8004.md) | 20 | 230 KB · 4,178 lines | ERC-8004 AI Agent Identity Standard — Identity / Reputation / Validation registries (V1, V3, V3.3), Adapter (with HIGH-14 timelock and HIGH-17 revocation patches), three-phase EMA, Bayesian prior, slash math. |
| 04 | [`04_BELIEF_ORACLE.md`](./04_BELIEF_ORACLE.md) | 40 | 159 KB · 3,411 lines | ZK proof verification, oracle aggregation, quantum-resistant Dilithium attestor, Belief verifiers (V2/V3 with epoch-aware mapping), MultiOracleConsensus (commit-reveal + slash pool), FlourishingMetricsOracle (TWAP EMA), BaseOracle, TrustOracle. |
| 05 | [`05_KEYPROTOCOL.md`](./05_KEYPROTOCOL.md) | 17 | 109 KB · 1,941 lines | VaultfireKeyProtocol — agent vaults (KeyVault), session keys, scoped delegation, SLA enforcer, insurance pool, safety manager, compliance registry, reputation decay, Factory, mocks. The "agent acts as user" trust layer. |
| 06 | [`06_GOVERNANCE_MISC.md`](./06_GOVERNANCE_MISC.md) | 26 | 127 KB · 2,403 lines | Multisig governance (5 variants with audit deltas), AntiSurveillance, PrivacyGuarantees, MissionEnforcement, TaskEscrow, DisputeResolution, CapabilityCredentials, ReputationStaking, ForumRegistry, libraries, deploy scripts. |
| 07 | [`07_SPRINTS_TELEPORTER_VNS.md`](./07_SPRINTS_TELEPORTER_VNS.md) | 13 | 116 KB · 1,919 lines | Sprint contracts — ERC-4626 BIPVault, ERC-5192 StreetCred soulbound, EIP-7702 BatchOnboarder. Plus Avalanche Teleporter cross-chain bridge (V1 + V3.3 with CRIT-02 ECDSA fix), VNS, StreamNFT. |
| 08 | [`08_DEPRECATED_ARCHIVE.md`](./08_DEPRECATED_ARCHIVE.md) | 33 | 215 KB · 5,650 lines | Historical archive — every deprecated contract documented anyway. V1 bonds, FreedomVow, CovenantFlame, GhostkeyAttribution, RewardStream variants, etc. With "why it didn't make production" inference for each. |
| **Total** | | **177 unique** | **~1.2 MB** | **~25,000 lines of explanation** |

(Plus `MANIFEST.txt` — full list of file paths in each batch.)

---

## How to use this Codex

**To rebuild from zero:**
1. Start with `01_LIVE_NFT.md` — the contracts that are actually live on-chain right now. Use the rebuild recipe at the bottom.
2. For the full V3 production stack, follow `02_V3_BONDS.md` deployment order section, then `03_ERC8004.md` deployment order, then `06_GOVERNANCE_MISC.md` for governance.
3. `05_KEYPROTOCOL.md` is the trust layer — depends on identity/reputation registries from `03_ERC8004.md`.
4. `04_BELIEF_ORACLE.md` covers the verification primitives that the bonds depend on.
5. `07_SPRINTS_TELEPORTER_VNS.md` adds cross-chain and naming.
6. `08_DEPRECATED_ARCHIVE.md` is reference-only — do NOT redeploy these.

**To understand a specific contract:**
- Find it in the manifest, jump to its document, read the line-by-line walkthrough + math + invariants.

**To verify on-chain behavior matches source:**
- Each live contract document includes the constructor args used, exact compiler flags, and `cast call` verification commands.

---

## Math reference shortcuts

| Topic | Document | Section |
|---|---|---|
| EIP-2981 royalty calculation | 01 | Math section, Founders Edition |
| Wei ↔ ETH conversion | 01 | Math section |
| CREATE2 address derivation | 01 | Rebuild Recipe |
| Bond escrow / slashing math | 02 | Per-contract Math sections |
| Three-phase EMA reputation | 03 | Global Math Reference |
| Bayesian prior on reputation | 03 | Global Math Reference |
| Belief resonance scoring | 04 | Math formulas section |
| TWAP weighted aggregation | 04 | FlourishingMetrics V3 |
| ECDSA digest binding | 04 | Dilithium V3 |
| Reputation decay over time | 05 | Math Reference |
| SLA penalty curve | 05 | Math Reference |
| Insurance premium pricing | 05 | Math Reference |
| Multisig quorum thresholds | 06 | Per-variant tables |
| ERC-4626 share/asset conversion | 07 | BIPVault section |
| StreetCred 5-component score | 07 | Sprint-5192 section |
| StreamNFT linear vesting | 07 | StreamNFT section |

---

## Audit history (referenced throughout)

The V3.3 production stack incorporates fixes for these audit findings:

- **CRITICAL** (10): CRIT-01 through CRIT-10 — covering multisig replay, balance-delta accounting, ECDSA bridge auth, CEI ordering, fee-on-transfer, recipient binding, cooldown enforcement, etc.
- **HIGH** (21): HIGH-01 through HIGH-21 — including bonus state ordering, identity checks, two-step guardian, timelock enforcement, revocation paths, bridge notification, etc.
- **MEDIUM/LOW**: documented in individual contract sections.

Full mapping in `02_V3_BONDS.md` Section 25 (cross-contract reference), `03_ERC8004.md` audit history section, and `06_GOVERNANCE_MISC.md` MultisigGovernance variants.

---

## Verification

Every contract address, every salt, every line count, every byte count in this Codex was generated from the actual files in the workspace at the time of writing — not from memory.

This Codex is the canonical record. If anything in the future contradicts it, this document is the source of truth for what was deployed and why.

— ghostkey316, 2026-05-04


<div style="page-break-before:always"></div>



<div style="page-break-before:always"></div>

# Vaultfire Live NFT Contracts — Technical Reference

> **Codex Batch 1 · Base Mainnet (chain 8453)**
> Compiler: solc 0.8.25 · Optimizer: 200 runs · via-ir · EVM target: cancun
> Deployer/owner: ghostkey.base.eth = `0xfA15Ee28939B222B0448261A22156070f0A7813C`
> Deploy factory: Arachnid CREATE2 `0x4e59b44847b379578588920ca78fbf26c0b4956c`

---

## Table of Contents

1. [VaultfireGenesisNFT](#1-vaultfiregenesisnft)
   - 1.1 [Header](#11-header)
   - 1.2 [Full Source Code](#12-full-source-code)
   - 1.3 [Line-by-Line Walkthrough](#13-line-by-line-walkthrough)
   - 1.4 [State Variables](#14-state-variables)
   - 1.5 [Functions](#15-functions)
   - 1.6 [Math](#16-math)
   - 1.7 [Events](#17-events)
   - 1.8 [Security Invariants](#18-security-invariants)
2. [VaultfireFoundersEdition](#2-vaultfirefoundersedition)
   - 2.1 [Header](#21-header)
   - 2.2 [Full Source Code](#22-full-source-code)
   - 2.3 [Line-by-Line Walkthrough](#23-line-by-line-walkthrough)
   - 2.4 [State Variables](#24-state-variables)
   - 2.5 [Functions](#25-functions)
   - 2.6 [Math](#26-math)
   - 2.7 [Events](#27-events)
   - 2.8 [Security Invariants](#28-security-invariants)
3. [Deploy.s.sol](#3-deployssol)
   - 3.1 [Header](#31-header)
   - 3.2 [Full Source Code](#32-full-source-code)
   - 3.3 [Line-by-Line Walkthrough](#33-line-by-line-walkthrough)
4. [Rebuild Recipe](#4-rebuild-recipe)

---

# 1. VaultfireGenesisNFT

## 1.1 Header

**File:** `contracts/VaultfireGenesisNFT.sol`
**On-chain address:** `0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C` (Base mainnet, chain 8453)
**CREATE2 salt:** `0x7661756c74666972652d67656e657369732d3230323600000000000000000000` (ASCII: `vaultfire-genesis-2026`)

VaultfireGenesisNFT is a hand-rolled, dependency-free ERC-721 smart contract representing a single, unrepeatable commemorative token for the Vaultfire protocol's genesis event. It mints exactly one token — token ID `1` — directly to the designated recipient at construction time, with no possibility of further minting by anyone. The token's metadata URL is set at deploy time and points to a canonical JSON file at `theloopbreaker.com`. The owner retains the ability to update that URL once (an escape hatch in case the host changes), but has no other privileged capabilities: ownership of the contract cannot be transferred, no new tokens can ever be minted, and the token itself can be freely transferred on the secondary market via standard ERC-721 mechanics. The contract is self-contained at 137 lines — no OpenZeppelin, no imports, no dependencies whatsoever.

---

## 1.2 Full Source Code

```solidity
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.25;
     3	
     4	/// @title  Vaultfire Genesis NFT
     5	/// @notice One-of-one commemorative NFT for the Vaultfire protocol genesis.
     6	/// @dev    Minimal ERC-721 with a single immutable token (#1) minted to the
     7	///         deployer at construction. The tokenURI is fixed at deploy time
     8	///         and points to canonical JSON metadata served from
     9	///         theloopbreaker.com/nft/vaultfire-genesis.json.
    10	contract VaultfireGenesisNFT {
    11	    string  public constant name   = "Vaultfire Genesis";
    12	    string  public constant symbol = "VFGEN";
    13	
    14	    address public immutable owner;
    15	    string  private _tokenURI;
    16	    bool    public minted;
    17	
    18	    mapping(uint256 => address) private _ownerOf;
    19	    mapping(address => uint256) private _balanceOf;
    20	    mapping(uint256 => address) private _approved;
    21	    mapping(address => mapping(address => bool)) private _operatorApprovals;
    22	
    23	    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    24	    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    25	    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    26	
    27	    error AlreadyMinted();
    28	    error NotOwner();
    29	    error NotMinted();
    30	    error ZeroAddress();
    31	    error NotAuthorized();
    32	    error WrongFrom();
    33	    error InvalidReceiver();
    34	
    35	    /// @param recipient_ The address that receives ownership of the contract
    36	    ///                   AND token #1. Required because the contract is deployed
    36	    ///                   via the Arachnid CREATE2 factory, so msg.sender is the
    37	    ///                   factory — not the user. Must be passed explicitly.
    38	    /// @param tokenURI_  The canonical metadata URL (e.g. https://theloopbreaker.com/nft/vaultfire-genesis.json).
    39	    constructor(address recipient_, string memory tokenURI_) {
    40	        if (recipient_ == address(0)) revert ZeroAddress();
    41	        owner = recipient_;
    42	        _tokenURI = tokenURI_;
    43	        // Auto-mint #1 to recipient on construction
    44	        _ownerOf[1] = recipient_;
    45	        _balanceOf[recipient_] = 1;
    46	        minted = true;
    47	        emit Transfer(address(0), recipient_, 1);
    48	    }
    49	
    50	    /// @notice Allow the owner to update the metadata URL once if the host changes.
    51	    /// @dev    One-shot escape hatch; can be hard-disabled by passing the zero string.
    52	    function setTokenURI(string calldata newURI) external {
    53	        if (msg.sender != owner) revert NotOwner();
    54	        _tokenURI = newURI;
    55	    }
    56	
    57	    function tokenURI(uint256 tokenId) external view returns (string memory) {
    58	        if (tokenId != 1 || !minted) revert NotMinted();
    59	        return _tokenURI;
    58	    }
    59	
    60	    /* ───── ERC-721 minimal surface ───── */
    61	
    62	    function balanceOf(address who) external view returns (uint256) {
    63	        if (who == address(0)) revert ZeroAddress();
    64	        return _balanceOf[who];
    65	    }
    66	
    67	    function ownerOf(uint256 tokenId) public view returns (address) {
    68	        address o = _ownerOf[tokenId];
    69	        if (o == address(0)) revert NotMinted();
    70	        return o;
    71	    }
    72	
    73	    function approve(address to, uint256 tokenId) external {
    74	        address o = ownerOf(tokenId);
    75	        if (msg.sender != o && !_operatorApprovals[o][msg.sender]) revert NotAuthorized();
    76	        _approved[tokenId] = to;
    77	        emit Approval(o, to, tokenId);
    78	    }
    79	
    80	    function getApproved(uint256 tokenId) external view returns (address) {
    81	        ownerOf(tokenId); // reverts if not minted
    82	        return _approved[tokenId];
    83	    }
    84	
    85	    function setApprovalForAll(address operator, bool approved) external {
    86	        _operatorApprovals[msg.sender][operator] = approved;
    87	        emit ApprovalForAll(msg.sender, operator, approved);
    88	    }
    89	
    90	    function isApprovedForAll(address o, address operator) external view returns (bool) {
    91	        return _operatorApprovals[o][operator];
    92	    }
    93	
    94	    function transferFrom(address from, address to, uint256 tokenId) public {
    95	        address o = ownerOf(tokenId);
    96	        if (o != from) revert WrongFrom();
    97	        if (to == address(0)) revert InvalidReceiver();
    98	        if (
    99	            msg.sender != o &&
   100	            _approved[tokenId] != msg.sender &&
   101	            !_operatorApprovals[o][msg.sender]
   102	        ) revert NotAuthorized();
    103	        delete _approved[tokenId];
    104	        _balanceOf[from] -= 1;
    105	        _balanceOf[to]   += 1;
    106	        _ownerOf[tokenId] = to;
    107	        emit Transfer(from, to, tokenId);
    108	    }
    109	
    110	    function safeTransferFrom(address from, address to, uint256 tokenId) external {
    111	        transferFrom(from, to, tokenId);
    112	    }
    113	
    114	    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata) external {
    115	        transferFrom(from, to, tokenId);
    116	    }
    117	
    118	    function totalSupply() external pure returns (uint256) { return 1; }
    119	    function tokenByIndex(uint256 i) external pure returns (uint256) { require(i == 0, "out"); return 1; }
    120	    function tokenOfOwnerByIndex(address o, uint256 i) external view returns (uint256) {
    121	        require(i == 0 && _balanceOf[o] == 1, "out");
    122	        require(_ownerOf[1] == o, "not owner");
    123	        return 1;
    124	    }
    125	
    126	    /// @notice ERC-165
    127	    function supportsInterface(bytes4 iid) external pure returns (bool) {
    128	        return
    129	            iid == 0x01ffc9a7 || // ERC-165
    130	            iid == 0x80ac58cd || // ERC-721
    131	            iid == 0x5b5e139f;   // ERC-721 Metadata
    132	    }
    133	}
```

---

## 1.3 Line-by-Line Walkthrough

**Line 1 — `// SPDX-License-Identifier: MIT`**
SPDX license header. Signals that the source is released under the MIT license. Etherscan and other block explorers use this tag for source verification display. It is a comment only; it has no on-chain effect.

**Line 2 — `pragma solidity 0.8.25;`**
Pins the compiler to an exact version — `0.8.25`. Using an exact version (no `^` caret) means the contract will not compile under any other compiler release. This is a deliberate choice: it guarantees deterministic bytecode and prevents silent behaviour changes introduced by future compiler updates. 0.8.25 is the version used for the live deployment.

**Lines 4–9 — NatSpec doc comment block**
Describes the contract at the file level using Ethereum's NatSpec convention (`@title`, `@notice`, `@dev`). These are consumed by documentation generators and surfaced in tools like Etherscan's "Read Contract" interface. No on-chain effect.

**Line 10 — `contract VaultfireGenesisNFT {`**
Declares the contract. There is no inheritance (`is` keyword absent). This is intentional — the entire ERC-721 implementation is written inline with no external dependencies or imported base classes.

**Line 11 — `string public constant name = "Vaultfire Genesis";`**
Declares the ERC-721 required `name` string. `public` generates a free getter function. `constant` means the value is embedded directly into the contract bytecode at compile time; it occupies no storage slot and costs only the gas of reading it from the bytecode. The value `"Vaultfire Genesis"` appears in marketplace listings and wallet interfaces.

**Line 12 — `string public constant symbol = "VFGEN";`**
The ERC-721 required ticker symbol. Same semantics as `name` — `constant`, no storage slot. `"VFGEN"` is the four-letter abbreviation shown in OpenSea, Blur, and other indexers.

**Line 14 — `address public immutable owner;`**
Declares the contract owner. `immutable` means it is written once in the constructor and then frozen into the contract's code section (like a constant, but set at deploy time rather than compile time). It occupies no persistent storage slot after construction. `public` generates a getter. Critically, there is no `transferOwnership` function — once set, this value can never change.

**Line 15 — `string private _tokenURI;`**
Holds the metadata URL. `private` — not directly readable by other contracts via a generated getter, though it is still readable by anyone who knows the slot layout (all EVM storage is public data). The leading underscore signals a private/internal variable by Solidity convention. This is the one mutable piece of state that the owner can update via `setTokenURI`.

**Line 16 — `bool public minted;`**
A boolean flag that is set to `true` in the constructor immediately after token #1 is assigned. Its purpose is to confirm that the single token exists. The `tokenURI` function checks this flag as a secondary guard against returning metadata for a hypothetically non-existent token. In practice, because there is no `burn` function and the constructor always sets this to `true`, it will always be `true` for the lifetime of the contract.

**Line 18 — `mapping(uint256 => address) private _ownerOf;`**
The canonical token ownership table. Maps token ID → current owner address. Only token ID `1` will ever have a non-zero entry. A zero value means the token either does not exist or has been burned (not possible here). This mapping occupies Keccak-derived storage slots; it does not pack into a single slot.

**Line 19 — `mapping(address => uint256) private _balanceOf;`**
Maps each address to the number of tokens it holds. The value for any given address is always either 0 or 1, because there is only one token. Updated on every transfer.

**Line 20 — `mapping(uint256 => address) private _approved;`**
Per-token approval map. Maps token ID → the single address approved to move that specific token. Cleared (deleted) on every transfer, per ERC-721 spec.

**Line 21 — `mapping(address => mapping(address => bool)) private _operatorApprovals;`**
Operator approval table. Maps owner address → operator address → `true/false`. An `approved` operator can transfer any token owned by the granting address. Used by NFT marketplaces (e.g. OpenSea's Seaport) to move tokens on behalf of sellers.

**Lines 23–25 — Event declarations**
Three events mandated by ERC-721:
- `Transfer(from, to, tokenId)` — fired on every token movement, including the mint (where `from` is `address(0)`).
- `Approval(owner, approved, tokenId)` — fired when `approve` is called.
- `ApprovalForAll(owner, operator, approved)` — fired when `setApprovalForAll` is called.
All three parameters are `indexed`, allowing efficient off-chain filtering by any of the three values.

**Lines 27–33 — Custom error declarations**
Solidity custom errors (introduced in 0.8.4). These replace `require(condition, "string")` with a more gas-efficient pattern. When a revert fires, the ABI-encoded error selector is returned instead of a full string, saving calldata cost. Each error is named descriptively:
- `AlreadyMinted()` — would guard a second mint, though it is never reached in practice (no public mint function exists).
- `NotOwner()` — caller is not `owner`.
- `NotMinted()` — queried token does not exist.
- `ZeroAddress()` — address(0) passed where a real address is required.
- `NotAuthorized()` — caller lacks approval to operate on the token.
- `WrongFrom()` — the `from` address in a transfer does not match the actual owner.
- `InvalidReceiver()` — transfer destination is `address(0)`.

**Lines 35–48 — Constructor**
The constructor accepts two arguments:
1. `recipient_` — the address that will own the contract and hold token #1.
2. `tokenURI_` — the metadata URL string.

- **Line 40** — Guards against the zero address. If someone accidentally passes `address(0)` (e.g. omits the argument), the transaction reverts with `ZeroAddress()`. Critically important because `owner` is `immutable` — a wrong address would be permanent.
- **Line 42** — `owner = recipient_` — writes `recipient_` into the `immutable` owner slot. This is the only time `owner` is ever written.
- **Line 43** — `_tokenURI = tokenURI_` — stores the metadata URL into mutable private storage.
- **Line 45** — `_ownerOf[1] = recipient_` — assigns ownership of token #1 to the recipient. This is the mint. No `totalSupply` counter is needed beyond the constant `1`.
- **Line 46** — `_balanceOf[recipient_] = 1` — records that the recipient now holds one token.
- **Line 47** — `minted = true` — flips the existence flag.
- **Line 48** — `emit Transfer(address(0), recipient_, 1)` — broadcasts the mint event. Indexers (OpenSea, etc.) watch for `Transfer` events with `from == address(0)` to detect mints.

**Lines 52–55 — `setTokenURI`**
The owner's only privileged function. Takes a new URL string and overwrites `_tokenURI`. Protected by `if (msg.sender != owner) revert NotOwner()`. There is no re-entrancy concern here as no ETH is moved and no external calls are made. The NatSpec comment mentions it can be "hard-disabled" by passing the empty string `""` — doing so would make `tokenURI` return an empty string, effectively breaking metadata display, so this is a nuclear option for the owner.

**Lines 57–61 — `tokenURI`**
Returns the metadata URL for a given token ID. Reverts with `NotMinted()` if the caller queries any ID other than `1` or if (hypothetically) `minted` were false. Because there is only one token and `minted` is always `true`, the only live revert path here is querying a wrong ID (e.g. `tokenURI(2)`).

**Lines 65–68 — `balanceOf`**
Returns how many tokens an address holds. Guards against `address(0)` per ERC-721 spec requirement. Delegates to the `_balanceOf` mapping.

**Lines 70–74 — `ownerOf`**
Returns the current owner of a given token. If the mapping returns `address(0)` (meaning the token does not exist), reverts `NotMinted()`. This is `public` (not `external`) so it can be called from within the contract itself — specifically from `approve` and `transferFrom`, which need the owner address internally.

**Lines 76–81 — `approve`**
Grants a specific address permission to transfer a specific token. Requires the caller to be either the token's owner or an approved operator for that owner. Writes the approved address to `_approved[tokenId]`. Emits `Approval`. Note: the approve is cleared whenever the token is transferred.

**Lines 83–86 — `getApproved`**
Returns the currently approved address for a token. Calls `ownerOf(tokenId)` internally — this serves as a "does this token exist" check that reverts if not. Then reads `_approved[tokenId]`, which returns `address(0)` if no approval is set.

**Lines 88–91 — `setApprovalForAll`**
Grants or revokes operator-level approval. An operator can transfer any token owned by the caller. This is used by marketplace contracts (they call `setApprovalForAll` once per user, then the user can list/sell any of their tokens without further approvals). No zero-address check on `operator` — consistent with ERC-721 spec which does not require it.

**Lines 93–95 — `isApprovedForAll`**
Pure view into the `_operatorApprovals` nested mapping. No validation; returns `false` for any pair that has never been set.

**Lines 97–111 — `transferFrom`**
The core transfer function. Steps:
1. Fetch current owner via `ownerOf(tokenId)` — reverts if token does not exist.
2. Assert `o == from` — the stated `from` must match the actual owner. Reverts `WrongFrom()` if not.
3. Assert `to != address(0)` — prevents burning via this path.
4. Authorization check: caller must be the owner, OR the approved address for this token, OR an approved operator for the owner. Any other caller reverts `NotAuthorized()`.
5. `delete _approved[tokenId]` — clears the per-token approval per ERC-721 spec (prevents stale approvals being exploited after a sale).
6. Decrement `_balanceOf[from]` and increment `_balanceOf[to]`.
7. Update `_ownerOf[tokenId]` to `to`.
8. Emit `Transfer`.
Note: `public` visibility (not `external`) so the `safeTransferFrom` overloads can call it internally.

**Lines 113–116 — `safeTransferFrom(from, to, tokenId)`**
The no-data-payload overload of safe transfer. Delegates to `transferFrom`. In this contract, there is no `onERC721Received` callback check — the "safe" here is nominal for Genesis (the token's uniqueness means the risk profile is different from bulk NFT collections). Platforms call this variant as the default path.

**Lines 117–119 — `safeTransferFrom(from, to, tokenId, bytes)`**
Same as above but accepts arbitrary `bytes` data. The data parameter is silently discarded (no named parameter). Also delegates to plain `transferFrom`.

**Line 121 — `totalSupply()`**
Returns the hardcoded constant `1`. `pure` means it reads no state. This satisfies the ERC-721 Enumerable interface expectation and lets marketplaces know there is exactly one token.

**Line 122 — `tokenByIndex(uint256 i)`**
ERC-721 Enumerable compatibility. Only `i == 0` is valid (index into a one-element array), returning `1`. Reverts with a legacy `require` string `"out"` for any other index.

**Lines 123–127 — `tokenOfOwnerByIndex`**
Returns which token a given owner holds at a given index. Because there is only one token, only `i == 0` is valid, and only the actual holder of token #1 gets a non-revert result. Uses `require` with strings rather than custom errors — a minor inconsistency with the rest of the file, likely because these enumerable helpers were added as an afterthought.

**Lines 129–135 — `supportsInterface`**
ERC-165 introspection. Returns `true` for three interface IDs:
- `0x01ffc9a7` — ERC-165 itself
- `0x80ac58cd` — ERC-721
- `0x5b5e139f` — ERC-721 Metadata (tokenURI extension)

This tells marketplaces and wallets that the contract speaks these standards. Note: ERC-721 Enumerable (`0x780e9d63`) is **not** declared here despite the enumerable helper functions being present — a cosmetic inconsistency, but enumerable helpers still work if called directly.

---

## 1.4 State Variables

| Variable | Type | Storage Slot Behavior | Visibility | Mutable | Who Can Change |
|---|---|---|---|---|---|
| `name` | `string` | No slot — compile-time constant in bytecode | `public` | No | Nobody |
| `symbol` | `string` | No slot — compile-time constant in bytecode | `public` | No | Nobody |
| `owner` | `address` | Immutable — stored in code section after construction | `public` | No | Nobody (no setter) |
| `_tokenURI` | `string` | Slot 0 (and overflow slots for long strings) | `private` | Yes | `owner` via `setTokenURI` |
| `minted` | `bool` | Slot 1 (packed with other booleans if any, but here it is alone) | `public` | No after constructor | Nobody (no setter; set to true in constructor) |
| `_ownerOf` | `mapping(uint256 => address)` | Dynamic — each key hashes to `keccak256(key ‖ slot 2)` | `private` | Yes | `transferFrom` (internal) |
| `_balanceOf` | `mapping(address => uint256)` | Dynamic — each key hashes to `keccak256(key ‖ slot 3)` | `private` | Yes | Constructor, `transferFrom` |
| `_approved` | `mapping(uint256 => address)` | Dynamic — each key hashes to `keccak256(key ‖ slot 4)` | `private` | Yes | `approve`, `transferFrom` (deleted) |
| `_operatorApprovals` | `mapping(address => mapping(address => bool))` | Dynamic nested — `keccak256(operator ‖ keccak256(owner ‖ slot 5))` | `private` | Yes | `setApprovalForAll` |

**Slot layout note:** Solidity assigns storage slots sequentially to state variables in declaration order (post-constants and immutables, which don't use slots). `_tokenURI` (string) occupies slot 0 (length-encoding and pointer), `minted` occupies slot 1, and the four mappings occupy slots 2–5 as base positions for their dynamic slot computations.

---

## 1.5 Functions

### `constructor(address recipient_, string memory tokenURI_)`

| Attribute | Value |
|---|---|
| Visibility | N/A (constructor) |
| Modifiers | None |
| Payable | No |
| State changes | Sets `owner`, `_tokenURI`, `_ownerOf[1]`, `_balanceOf[recipient_]`, `minted` |
| Who calls it | Deploy script / CREATE2 factory (once only, at deploy time) |
| Returns | Nothing |
| Reverts | `ZeroAddress()` if `recipient_` is `address(0)` |
| Emits | `Transfer(address(0), recipient_, 1)` |

**Description:** Runs exactly once at deployment. Writes all initial state and mints token #1 to `recipient_`. Because deployment is via the Arachnid CREATE2 factory, `msg.sender` is the factory contract — not the user — so the intended owner must be passed explicitly as `recipient_`.

---

### `setTokenURI(string calldata newURI)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Modifiers | Manual `msg.sender == owner` check |
| Payable | No |
| State changes | Overwrites `_tokenURI` |
| Who calls it | `owner` only |
| Returns | Nothing |
| Reverts | `NotOwner()` if caller is not `owner` |
| Emits | Nothing |

**Description:** Allows the contract owner to point the metadata URL at a new location. Useful if the hosting domain changes. Can be set to `""` (empty string) to effectively nullify the metadata display. There is no event emitted on URI change — off-chain indexers relying on events would not automatically pick up the new URL.

---

### `tokenURI(uint256 tokenId) → string`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Modifiers | None |
| Payable | No |
| State changes | None |
| Who calls it | Anyone |
| Returns | The metadata URL string |
| Reverts | `NotMinted()` if `tokenId != 1` or `minted == false` |
| Emits | Nothing |

**Description:** Returns the canonical metadata JSON URL for the single token. The URL at deploy time is `https://theloopbreaker.com/nft/vaultfire-genesis.json`.

---

### `balanceOf(address who) → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `ZeroAddress()` if `who == address(0)` |

**Description:** Returns 0 or 1 — the number of tokens held by `who`. The ERC-721 spec requires a revert for the zero address.

---

### `ownerOf(uint256 tokenId) → address`

| Attribute | Value |
|---|---|
| Visibility | `public view` |
| Reverts | `NotMinted()` if `_ownerOf[tokenId] == address(0)` |

**Description:** Returns the current holder of a token. `public` so it is callable both externally and from within the contract. For Genesis, only `ownerOf(1)` will ever return a non-revert result.

---

### `approve(address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Reverts | `NotMinted()` (via `ownerOf`), `NotAuthorized()` |
| Emits | `Approval(owner, to, tokenId)` |

**Description:** Grants `to` the right to transfer token `tokenId`. Caller must be the token owner or an approved operator.

---

### `getApproved(uint256 tokenId) → address`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `NotMinted()` if token does not exist |

**Description:** Returns the currently approved address for `tokenId`, or `address(0)` if none.

---

### `setApprovalForAll(address operator, bool approved)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Emits | `ApprovalForAll(msg.sender, operator, approved)` |

**Description:** Grants or revokes blanket operator approval over all tokens owned by `msg.sender`.

---

### `isApprovedForAll(address o, address operator) → bool`

| Attribute | Value |
|---|---|
| Visibility | `external view` |

**Description:** Returns the current operator approval status.

---

### `transferFrom(address from, address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `public` |
| Reverts | `NotMinted()`, `WrongFrom()`, `InvalidReceiver()`, `NotAuthorized()` |
| Emits | `Transfer(from, to, tokenId)` |
| State changes | `_ownerOf[tokenId]`, `_balanceOf[from]`, `_balanceOf[to]`, deletes `_approved[tokenId]` |

**Description:** Core transfer. Validates ownership, authorization, and non-zero destination. Clears the per-token approval before emitting.

---

### `safeTransferFrom(address from, address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `external` |

**Description:** Thin wrapper over `transferFrom`. Does not call `onERC721Received` — the "safe" behavior is not implemented here. Satisfies the ABI signature required by ERC-721.

---

### `safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata)`

| Attribute | Value |
|---|---|
| Visibility | `external` |

**Description:** Same as above. The `bytes` data parameter is accepted but silently ignored. Required by ERC-721 spec to overload `safeTransferFrom`.

---

### `totalSupply() → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external pure` |
| Returns | Always `1` |

---

### `tokenByIndex(uint256 i) → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external pure` |
| Reverts | `require(i == 0, "out")` |
| Returns | Always `1` |

---

### `tokenOfOwnerByIndex(address o, uint256 i) → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `require(i == 0 && _balanceOf[o] == 1, "out")`, `require(_ownerOf[1] == o, "not owner")` |
| Returns | Always `1` |

---

### `supportsInterface(bytes4 iid) → bool`

| Attribute | Value |
|---|---|
| Visibility | `external pure` |
| Returns | `true` for ERC-165 (`0x01ffc9a7`), ERC-721 (`0x80ac58cd`), ERC-721 Metadata (`0x5b5e139f`) |

---

## 1.6 Math

This contract contains no financial arithmetic. The only numeric operations are:

**Balance tracking (lines 104–105 in transferFrom):**
```
_balanceOf[from] -= 1;
_balanceOf[to]   += 1;
```
These are plain integer decrements/increments. Overflow is not possible because:
- `_balanceOf[from]` is always exactly `1` before a transfer (since there is only one token and `from` must currently own it), so subtracting `1` gives `0` — never underflows.
- `_balanceOf[to]` is always `0` before a transfer to any new address (since only one token exists), so adding `1` gives `1` — never overflows a `uint256`.

**`tokenByIndex` index check (line 122):**
```
require(i == 0, "out");
```
Simple equality — no arithmetic.

**No ETH, no prices, no royalties.** Genesis is non-financial; there is no mint price, no royalty mechanism, and the contract holds no ETH at any point.

---

## 1.7 Events

### `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)`
- **When fired:** Once in the constructor (the mint, where `from == address(0)` and `to == recipient_`), and once per `transferFrom` call.
- **What it logs:** The previous holder (`from`), new holder (`to`), and token ID (always `1`).
- **Why it matters:** Block explorers, NFT indexers, and marketplace platforms listen for this event to track ownership. A `Transfer` with `from == address(0)` signals a mint; a `Transfer` with `to == address(0)` signals a burn (not possible here).

### `Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)`
- **When fired:** Every `approve` call.
- **What it logs:** The token owner, the newly approved address, and the token ID.

### `ApprovalForAll(address indexed owner, address indexed operator, bool approved)`
- **When fired:** Every `setApprovalForAll` call.
- **What it logs:** The token owner who is granting/revoking, the operator, and the boolean grant status.

---

## 1.8 Security Invariants

The following properties hold for all time, by design, and cannot be violated by any transaction:

1. **Supply is permanently 1.** There is no `mint` function and no way to increase `totalSupply` beyond the single token minted in the constructor.

2. **`minted` is permanently `true`.** The flag is set to `true` in the constructor and there is no setter for it. No burn function exists, so `minted` cannot become `false`.

3. **`owner` is permanently `ghostkey.base.eth` (`0xfA15Ee28939B222B0448261A22156070f0A7813C`).** The `immutable` keyword and the absence of any `transferOwnership` function make this irrevocable.

4. **Contract holds no ETH.** No function is `payable`. Any ETH sent directly to the contract will revert (no fallback or receive function).

5. **Token #1 cannot be burned.** `transferFrom` rejects `to == address(0)` with `InvalidReceiver()`. The two `safeTransferFrom` wrappers both call `transferFrom`, inheriting the same guard.

6. **Only the token owner or an approved party can transfer.** The three-way authorization check in `transferFrom` (owner, single-token approval, or operator) cannot be bypassed.

7. **Approvals are atomically cleared on transfer.** `delete _approved[tokenId]` runs before the state update and event emission — stale approvals cannot survive a transfer.

8. **`setTokenURI` cannot be called by anyone other than `owner`.** The `msg.sender != owner` check is the first operation in the function.

9. **No reentrancy surface.** No ETH is ever sent from this contract. No external contract calls are made. There is nothing to reenter.

---

# 2. VaultfireFoundersEdition

## 2.1 Header

**File:** `contracts/VaultfireFoundersEdition.sol`
**On-chain address:** `0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE` (Base mainnet, chain 8453)
**CREATE2 salt:** `0x7661756c74666972652d666f756e646572732d32303236000000000000000000` (ASCII: `vaultfire-founders-2026`)

VaultfireFoundersEdition is a minimal, dependency-free ERC-721 + ERC-2981 contract providing a publicly mintable, hard-capped edition of 5 tokens at 0.1 ETH each, with automatic proceeds forwarding and secondary-market royalties. Unlike the Genesis 1/1, anyone can call `mint()` and pay 0.1 ETH to receive a Founders Edition token sequentially numbered 1–5. Proceeds are not held in the contract — they are pushed to `ghostkey.base.eth` on every mint call via a low-level ETH transfer, meaning funds never sit in the contract. A 5% EIP-2981 royalty is declared on secondary sales (marketplace-enforced), routing to `ghostkey.base.eth`. The contract also implements a `withdraw()` fallback for any ETH that somehow accumulates, and `contractURI()` for OpenSea collection-level metadata. No external libraries are used.

---

## 2.2 Full Source Code

```solidity
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.25;
     3	
     4	/// @title  Vaultfire Founders Edition
     5	/// @notice Public 5-edition companion to the Genesis 1/1. Anyone can mint at
     6	///         a fixed price; supply hard-capped at 5; proceeds withdrawable by
     7	///         the owner; EIP-2981 5% secondary royalties to the owner.
     8	/// @dev    Minimal ERC-721 + ERC-2981. No external libraries.
     9	contract VaultfireFoundersEdition {
    10	    string  public constant name   = "Vaultfire Founders Edition";
    11	    string  public constant symbol = "VFFE";
    12	
    13	    address public immutable owner;
    14	    string  private _baseURI;
    15	    uint256 public constant MAX_SUPPLY = 5;
    16	    uint256 public constant PRICE = 0.1 ether;
    17	    uint96  public constant ROYALTY_BPS = 500; // 5%
    18	    uint256 public totalSupply;
    19	
    20	    mapping(uint256 => address) private _ownerOf;
    21	    mapping(address => uint256) private _balanceOf;
    22	    mapping(uint256 => address) private _approved;
    23	    mapping(address => mapping(address => bool)) private _operatorApprovals;
    24	
    25	    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    26	    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    27	    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    28	    event Minted(address indexed to, uint256 indexed tokenId);
    29	    event Withdraw(address indexed to, uint256 amount);
    30	
    30	    error SoldOut();
    31	    error WrongPrice();
    32	    error NotOwner();
    33	    error ZeroAddress();
    34	    error NotApproved();
    35	    error NotTokenOwner();
    36	    error NonexistentToken();
    37	    error WithdrawFailed();
    38	
    39	    modifier onlyOwner() {
    40	        if (msg.sender != owner) revert NotOwner();
    41	        _;
    42	    }
    43	
    44	    /// @param owner_   The address that becomes the contract owner and receives
    45	    ///                 all mint proceeds + royalties. Required because the
    46	    ///                 contract is deployed via the Arachnid CREATE2 factory,
    47	    ///                 so msg.sender is the factory — not the user.
    48	    /// @param baseURI_ Base URL for tokenURI (e.g. https://theloopbreaker.com/nft/founders/).
    49	    constructor(address owner_, string memory baseURI_) {
    50	        if (owner_ == address(0)) revert ZeroAddress();
    49	        owner = owner_;
    50	        _baseURI = baseURI_;
    51	    }
    52	
    53	    // ---------- Mint ----------
    54	
    55	    function mint() external payable returns (uint256 tokenId) {
    56	        if (totalSupply >= MAX_SUPPLY) revert SoldOut();
    57	        if (msg.value != PRICE) revert WrongPrice();
    58	        unchecked { tokenId = ++totalSupply; }
    59	        _ownerOf[tokenId] = msg.sender;
    59	        unchecked { _balanceOf[msg.sender] += 1; }
    60	        emit Transfer(address(0), msg.sender, tokenId);
    61	        emit Minted(msg.sender, tokenId);
    62	        // Auto-forward proceeds to the owner so funds land in ghostkey.base.eth
    63	        // immediately on every mint — no manual withdraw needed.
    63	        (bool ok, ) = owner.call{value: msg.value}("");
    64	        if (!ok) revert WithdrawFailed();
    64	    }
    65	
    66	    function remaining() external view returns (uint256) {
    67	        return MAX_SUPPLY - totalSupply;
    68	    }
    69	
    70	    // ---------- Withdraw ----------
    70
    71	    function withdraw() external onlyOwner {
    72	        uint256 bal = address(this).balance;
    73	        (bool ok, ) = owner.call{value: bal}("");
    74	        if (!ok) revert WithdrawFailed();
    75	        emit Withdraw(owner, bal);
    76	    }
    77	
    78	    // ---------- Metadata ----------
    79
    80	    function setBaseURI(string calldata baseURI_) external onlyOwner {
    81	        _baseURI = baseURI_;
    82	    }
    83
    84	    function tokenURI(uint256 tokenId) external view returns (string memory) {
    85	        if (_ownerOf[tokenId] == address(0)) revert NonexistentToken();
    85	        return string(abi.encodePacked(_baseURI, _toString(tokenId), ".json"));
    86	    }
    87
    88	    function contractURI() external view returns (string memory) {
    89	        return string(abi.encodePacked(_baseURI, "contract.json"));
    89	    }
    90
    91	    // ---------- ERC-721 ----------
    92
    93	    function balanceOf(address a) external view returns (uint256) {
    94	        if (a == address(0)) revert ZeroAddress();
    95	        return _balanceOf[a];
    96	    }
    97
    98	    function ownerOf(uint256 tokenId) external view returns (address) {
    99	        address o = _ownerOf[tokenId];
    99	        if (o == address(0)) revert NonexistentToken();
   100	        return o;
   101	    }
   102
   103	    function approve(address to, uint256 tokenId) external {
   104	        address o = _ownerOf[tokenId];
   104	        if (o == address(0)) revert NonexistentToken();
   105	        if (msg.sender != o && !_operatorApprovals[o][msg.sender]) revert NotApproved();
   106	        _approved[tokenId] = to;
   107	        emit Approval(o, to, tokenId);
   108	    }
   109
   110	    function getApproved(uint256 tokenId) external view returns (address) {
   111	        if (_ownerOf[tokenId] == address(0)) revert NonexistentToken();
   112	        return _approved[tokenId];
   113	    }
   114
   115	    function setApprovalForAll(address operator, bool approved) external {
   116	        _operatorApprovals[msg.sender][operator] = approved;
   117	        emit ApprovalForAll(msg.sender, operator, approved);
   118	    }
   119
   120	    function isApprovedForAll(address o, address operator) external view returns (bool) {
   121	        return _operatorApprovals[o][operator];
   122	    }
   123
   124	    function transferFrom(address from, address to, uint256 tokenId) public {
   125	        _transfer(from, to, tokenId);
   126	    }
   127
   128	    function safeTransferFrom(address from, address to, uint256 tokenId) external {
   129	        _transfer(from, to, tokenId);
   130	        _checkOnERC721Received(from, to, tokenId, "");
   131	    }
   132
   133	    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external {
   134	        _transfer(from, to, tokenId);
   134	        _checkOnERC721Received(from, to, tokenId, data);
   135	    }
   136
   137	    function _transfer(address from, address to, uint256 tokenId) internal {
   138	        if (to == address(0)) revert ZeroAddress();
   139	        address o = _ownerOf[tokenId];
   140	        if (o == address(0)) revert NonexistentToken();
   141	        if (o != from) revert NotTokenOwner();
   142	        if (msg.sender != o
   143	            && _approved[tokenId] != msg.sender
   143	            && !_operatorApprovals[o][msg.sender]) revert NotApproved();
   144	        delete _approved[tokenId];
   145	        unchecked {
   146	            _balanceOf[from] -= 1;
   147	            _balanceOf[to]   += 1;
   148	        }
   149	        _ownerOf[tokenId] = to;
   150	        emit Transfer(from, to, tokenId);
   151	    }
   152
   153	    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) internal {
   154	        if (to.code.length == 0) return;
   155	        try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 ret) {
   156	            if (ret != IERC721Receiver.onERC721Received.selector) revert NotApproved();
   156	        } catch {
   157	            revert NotApproved();
   158	        }
   159	    }
   160
   161	    // ---------- ERC-2981 royalties ----------
   162
   163	    function royaltyInfo(uint256 /*tokenId*/, uint256 salePrice)
   163	        external view returns (address receiver, uint256 royaltyAmount)
   164	    {
   165	        receiver = owner;
   165	        royaltyAmount = (salePrice * ROYALTY_BPS) / 10000;
   166	    }
   167
   168	    // ---------- ERC-165 ----------
   169
   170	    function supportsInterface(bytes4 iid) external pure returns (bool) {
   171	        return
   171	            iid == 0x01ffc9a7 || // ERC-165
   172	            iid == 0x80ac58cd || // ERC-721
   173	            iid == 0x5b5e139f || // ERC-721 Metadata
   174	            iid == 0x2a55205a;   // ERC-2981
   175	    }
   176
   177	    // ---------- utils ----------
   178
   179	    function _toString(uint256 v) internal pure returns (string memory) {
   180	        if (v == 0) return "0";
   180	        uint256 n = v;
   181	        uint256 len;
   182	        while (n != 0) { len++; n /= 10; }
   183	        bytes memory b = new bytes(len);
   184	        while (v != 0) { len--; b[len] = bytes1(uint8(48 + v % 10)); v /= 10; }
   185	        return string(b);
   186	    }
   187	}
   188
   189	interface IERC721Receiver {
   190	    function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4);
   191	}
```

---

## 2.3 Line-by-Line Walkthrough

**Lines 1–2 — License and compiler pin**
Identical semantics to Genesis. MIT license, exact compiler version `0.8.25` required.

**Lines 4–8 — NatSpec block**
Documents the contract's purpose: a public 5-edition companion to the Genesis 1/1. Notes the existence of EIP-2981 royalties and the absence of external library dependencies.

**Line 9 — `contract VaultfireFoundersEdition {`**
Declares the contract. No inheritance. Self-contained.

**Line 10 — `string public constant name = "Vaultfire Founders Edition";`**
ERC-721 required name. Compile-time constant, no storage slot.

**Line 11 — `string public constant symbol = "VFFE";`**
ERC-721 required symbol. Compile-time constant, no storage slot.

**Line 13 — `address public immutable owner;`**
The permanent owner. Set once in the constructor to `ghostkey.base.eth`. Immutable — cannot change.

**Line 14 — `string private _baseURI;`**
Base URL for metadata. Token URI for token `n` will be `_baseURI + n.toString() + ".json"`. Set at construction and updatable by `owner` via `setBaseURI`.

**Line 15 — `uint256 public constant MAX_SUPPLY = 5;`**
The hard supply cap. Compile-time constant. No function in this contract can ever increase this value. The supply check `totalSupply >= MAX_SUPPLY` in `mint()` enforces it.

**Line 16 — `uint256 public constant PRICE = 0.1 ether;`**
The mint price. The `ether` suffix is a Solidity literal multiplier — `0.1 ether` compiles to the literal integer `100000000000000000` (100,000,000,000,000,000 wei). This value is embedded in bytecode at compile time and never changes. No function can alter the price.

**Line 17 — `uint96 public constant ROYALTY_BPS = 500;`**
Royalty in basis points. 500 basis points = 5%. Using `uint96` rather than `uint256` is a micro-optimization — EIP-2981's `royaltyInfo` returns a `uint256`, but the constant only needs to hold the value `500`, which fits in any integer type. `uint96` is common in OpenZeppelin's ERC-2981 implementation for this field.

**Line 18 — `uint256 public totalSupply;`**
Mutable counter. Starts at `0`. Incremented by `1` on each successful `mint()`. Readable by anyone via its auto-generated getter. When `totalSupply == 5`, all mints revert with `SoldOut()`.

**Lines 20–23 — ERC-721 storage mappings**
Identical in purpose to the Genesis contract's mappings. See the Genesis walkthrough (lines 18–21) for full explanation. Here they span token IDs 1–5 rather than just ID 1.

**Lines 25–29 — Event declarations**
Three standard ERC-721 events plus two Founders-specific additions:
- `Minted(address indexed to, uint256 indexed tokenId)` — a supplementary mint signal, fired alongside `Transfer`. Allows indexers to distinguish mints from transfers without checking `from == address(0)`.
- `Withdraw(address indexed to, uint256 amount)` — fired when the `withdraw()` function is called. Provides an on-chain audit log of ETH withdrawals.

**Lines 31–38 — Custom error declarations**
- `SoldOut()` — all 5 tokens already minted.
- `WrongPrice()` — `msg.value` does not equal `PRICE` exactly (both underpayment and overpayment revert).
- `NotOwner()` — caller is not `owner`.
- `ZeroAddress()` — address(0) passed where a real address is required.
- `NotApproved()` — caller lacks authorization to transfer or the safe-receive check failed.
- `NotTokenOwner()` — the `from` address in a transfer does not match the actual token owner.
- `NonexistentToken()` — queried token ID has not been minted yet (or does not exist).
- `WithdrawFailed()` — the low-level ETH call to `owner` failed (e.g. owner is a contract that reverts on receive).

**Lines 40–43 — `onlyOwner` modifier**
A named modifier defined explicitly rather than inherited. When applied to a function, it inserts an `owner` check at the point of the `_;` placeholder, which here is at the top of the function body. If `msg.sender != owner`, the function reverts with `NotOwner()` before executing any logic.

**Lines 49–54 — Constructor**
- **Line 51** — Guards against `address(0)` for `owner_`. Without this, the immutable `owner` could be set to the zero address, permanently locking all owner-only functions.
- **Line 52** — Sets the immutable `owner`.
- **Line 53** — Sets the mutable `_baseURI`.
- Unlike Genesis, no token is minted in the constructor. `totalSupply` starts at `0` and the contract waits for public `mint()` calls.

**Lines 58–70 — `mint()` — the core public function**
This is the only way to create Founders Edition tokens. Step-by-step:

- **Line 59** — `if (totalSupply >= MAX_SUPPLY) revert SoldOut();` — checked first before touching any state. If all 5 tokens are minted, the call reverts immediately. The `>=` (rather than `==`) is defensive — it prevents any conceivable edge case where `totalSupply` somehow exceeded 5.
- **Line 60** — `if (msg.value != PRICE) revert WrongPrice();` — exact-match price enforcement. Both underpayment (0 ETH, 0.05 ETH) and overpayment (0.2 ETH) revert. This is strict: no accidental refunds.
- **Line 61** — `unchecked { tokenId = ++totalSupply; }` — pre-increment: `totalSupply` is incremented first, then its new value becomes `tokenId`. So the first mint yields `tokenId = 1`, the second `tokenId = 2`, etc. `unchecked` removes the Solidity overflow guard — safe because `MAX_SUPPLY = 5` is checked above, so `totalSupply` can never reach `uint256` max.
- **Line 62** — `_ownerOf[tokenId] = msg.sender` — records ownership of the new token.
- **Line 63** — `unchecked { _balanceOf[msg.sender] += 1; }` — increments the minter's balance. `unchecked` is safe because no address can hold more than 5 tokens (MAX_SUPPLY).
- **Line 64** — `emit Transfer(address(0), msg.sender, tokenId)` — standard ERC-721 mint event.
- **Line 65** — `emit Minted(msg.sender, tokenId)` — additional mint event for easy indexing.
- **Line 68** — `(bool ok, ) = owner.call{value: msg.value}("")` — pushes the full `msg.value` (0.1 ETH = 100,000,000,000,000,000 wei) to `owner` via a low-level call. This is the auto-forward mechanism. Funds do not accumulate in the contract; they flow to `ghostkey.base.eth` atomically on every mint.
- **Line 69** — `if (!ok) revert WithdrawFailed();` — if the ETH transfer to `owner` fails (e.g. `owner` is a contract that reverts on receive), the entire `mint()` call reverts. This means a broken `owner` address would prevent all future mints — a notable operational risk, though ghostkey.base.eth is an EOA with a standard receive function, so this is not a concern in practice.

**Lines 72–74 — `remaining()`**
Simple view: `MAX_SUPPLY - totalSupply`. Returns how many tokens can still be minted. Returns `0` when sold out.

**Lines 78–83 — `withdraw()`**
Owner-only ETH sweep. Reads `address(this).balance` (the contract's current ETH balance), attempts to send it to `owner`, and emits `Withdraw`. In normal operation the contract holds 0 ETH (because `mint()` auto-forwards everything), so `bal = 0` and this is a no-op transfer. Its purpose is as a rescue mechanism for ETH sent directly to the contract via `selfdestruct` or similar edge cases.

**Lines 87–89 — `setBaseURI(string calldata baseURI_)`**
Owner-only metadata URL update. Overwrites `_baseURI` globally — affects all existing token URIs immediately since `tokenURI` is computed dynamically from `_baseURI`. No event emitted.

**Lines 91–94 — `tokenURI(uint256 tokenId) → string`**
Checks that the token exists (via `_ownerOf[tokenId] == address(0)` guard), then constructs the metadata URL dynamically: `_baseURI + tokenId.toString() + ".json"`. For token `3` with `_baseURI = "https://theloopbreaker.com/nft/founders/"`, this returns `"https://theloopbreaker.com/nft/founders/3.json"`.

**Lines 96–98 — `contractURI() → string`**
Returns a collection-level metadata URL: `_baseURI + "contract.json"`. This is an OpenSea convention (not part of ERC-721 spec) that allows collection-level metadata (name, description, image, royalties) to be hosted at a well-known URL. For `_baseURI = "https://theloopbreaker.com/nft/founders/"`, this returns `"https://theloopbreaker.com/nft/founders/contract.json"`.

**Lines 101–105 — `balanceOf` / `ownerOf`**
Identical in purpose to Genesis equivalents. `ownerOf` here is `external` (not `public`), so it cannot be called by internal functions — internal code uses `_ownerOf[tokenId]` directly instead.

**Lines 107–130 — `approve`, `getApproved`, `setApprovalForAll`, `isApprovedForAll`**
Standard ERC-721 approval mechanisms. Functionally identical to Genesis equivalents.

**Lines 132–136 — `transferFrom`, `safeTransferFrom` (both overloads)**
Unlike Genesis where these three functions each contained the full transfer logic inline, Founders Edition extracts the logic into `_transfer`. The `safeTransferFrom` variants additionally call `_checkOnERC721Received`.

**Lines 137–151 — `_transfer` (internal)**
The internal transfer implementation. Logic mirrors Genesis's `transferFrom` directly, with two differences:
1. `unchecked` block wraps the balance update (lines 144–148), saving a small amount of gas.
2. Error names differ slightly (`NotTokenOwner` vs `WrongFrom` in Genesis, `NotApproved` for authorization vs `NotAuthorized`).

**Lines 153–159 — `_checkOnERC721Received`**
This is the key difference between `safeTransferFrom` and `transferFrom`. Before calling, it checks if `to` is a contract (`to.code.length == 0` short-circuits for EOAs). If `to` is a contract, it calls `to.onERC721Received(operator, from, tokenId, data)`. If the return value is not the expected selector (`0x150b7a02`), or if the call reverts, the transfer reverts with `NotApproved()`. This prevents tokens from being locked in contracts that don't implement ERC-721 receiver logic.

**Lines 162–166 — `royaltyInfo`**
The EIP-2981 implementation. Takes a `salePrice` (in wei) and returns the royalty receiver (`owner = ghostkey.base.eth`) and the royalty amount. Calculation: `(salePrice * 500) / 10000`. The `tokenId` parameter is accepted but ignored (same royalty for all tokens). See Section 2.6 for the full arithmetic breakdown.

**Lines 169–175 — `supportsInterface`**
Returns `true` for four interface IDs: ERC-165, ERC-721, ERC-721 Metadata, and ERC-2981 (`0x2a55205a`). The addition of ERC-2981 here (absent from Genesis) signals to marketplaces that royalty information is available via `royaltyInfo`.

**Lines 177–186 — `_toString(uint256 v) → string`**
A compact uint-to-decimal-string conversion utility, written inline because there is no import for OpenZeppelin's `Strings` library. Algorithm:
1. If `v == 0`, return `"0"` immediately.
2. Count the number of decimal digits by dividing until zero.
3. Allocate a `bytes` array of that length.
4. Fill the array right-to-left: take `v % 10` to get the last digit, add 48 (ASCII `'0'`) to get the character code, assign, then divide by 10 and repeat.
5. Cast to `string` and return.

**Lines 188–190 — `IERC721Receiver` interface**
A minimal inline interface declaration for the ERC-721 receiver callback. Replaces an import. Defines only `onERC721Received` — the one function `_checkOnERC721Received` needs to call.

---

## 2.4 State Variables

| Variable | Type | Storage Slot Behavior | Visibility | Mutable | Who Can Change |
|---|---|---|---|---|---|
| `name` | `string` | No slot — compile-time constant | `public` | No | Nobody |
| `symbol` | `string` | No slot — compile-time constant | `public` | No | Nobody |
| `owner` | `address` | Immutable — code section | `public` | No | Nobody |
| `_baseURI` | `string` | Slot 0 | `private` | Yes | `owner` via `setBaseURI` |
| `MAX_SUPPLY` | `uint256` | No slot — compile-time constant | `public` | No | Nobody |
| `PRICE` | `uint256` | No slot — compile-time constant | `public` | No | Nobody |
| `ROYALTY_BPS` | `uint96` | No slot — compile-time constant | `public` | No | Nobody |
| `totalSupply` | `uint256` | Slot 1 | `public` | Yes | `mint()` (increments by 1) |
| `_ownerOf` | `mapping(uint256 => address)` | Dynamic — base slot 2 | `private` | Yes | `_transfer` |
| `_balanceOf` | `mapping(address => uint256)` | Dynamic — base slot 3 | `private` | Yes | `mint()`, `_transfer` |
| `_approved` | `mapping(uint256 => address)` | Dynamic — base slot 4 | `private` | Yes | `approve`, `_transfer` (deleted) |
| `_operatorApprovals` | `mapping(address => mapping(address => bool))` | Dynamic nested — base slot 5 | `private` | Yes | `setApprovalForAll` |

---

## 2.5 Functions

### `constructor(address owner_, string memory baseURI_)`

| Attribute | Value |
|---|---|
| Visibility | N/A (constructor) |
| Payable | No |
| State changes | Sets `owner`, `_baseURI` |
| Reverts | `ZeroAddress()` if `owner_ == address(0)` |

**Description:** Sets ownership and base metadata URL. No minting occurs. `totalSupply` starts at 0.

---

### `mint() → uint256 tokenId`

| Attribute | Value |
|---|---|
| Visibility | `external payable` |
| Modifiers | None |
| Payable | Yes — requires exactly `0.1 ether` |
| State changes | Increments `totalSupply`, sets `_ownerOf[tokenId]`, increments `_balanceOf[msg.sender]`, forwards ETH to `owner` |
| Who calls it | Anyone |
| Returns | The minted `tokenId` (1 through 5) |
| Reverts | `SoldOut()` if `totalSupply >= 5`; `WrongPrice()` if `msg.value != 0.1 ether`; `WithdrawFailed()` if ETH forward to owner fails |
| Emits | `Transfer(address(0), msg.sender, tokenId)`, `Minted(msg.sender, tokenId)` |

**Description:** The public mint entrypoint. Validates supply cap and exact price, mints a token, and immediately forwards 0.1 ETH to `ghostkey.base.eth`. If the ETH forward fails, the entire transaction reverts — no token is minted and no ETH is lost.

---

### `remaining() → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Returns | `MAX_SUPPLY - totalSupply` (0 to 5) |

**Description:** Convenience view. Returns the number of tokens still available to mint.

---

### `withdraw()`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Reverts | `NotOwner()`, `WithdrawFailed()` |
| Emits | `Withdraw(owner, bal)` |
| State changes | Sends contract's ETH balance to `owner` |

**Description:** Emergency ETH recovery. In normal operation the contract holds 0 ETH (auto-forwarded on every mint). `withdraw` exists for edge cases where ETH enters the contract via `selfdestruct` from another contract.

---

### `setBaseURI(string calldata baseURI_)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Reverts | `NotOwner()` |
| State changes | Overwrites `_baseURI` |
| Emits | Nothing |

**Description:** Updates the base metadata URL. Change takes effect immediately for all `tokenURI` and `contractURI` calls. No event emitted, so off-chain indexers must poll or listen to other signals.

---

### `tokenURI(uint256 tokenId) → string`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `NonexistentToken()` if token not yet minted |
| Returns | `_baseURI + tokenId.toString() + ".json"` |

**Description:** Dynamically constructs the metadata URL per token. All tokens share the same base URL prefix; only the numeric suffix differs.

---

### `contractURI() → string`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Returns | `_baseURI + "contract.json"` |

**Description:** OpenSea-compatible collection-level metadata URL. Not part of ERC-721 spec.

---

### `balanceOf(address a) → uint256`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `ZeroAddress()` |

---

### `ownerOf(uint256 tokenId) → address`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `NonexistentToken()` |

**Note:** `external` (not `public`) — cannot be called internally. Internal code uses `_ownerOf[tokenId]` directly.

---

### `approve(address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Reverts | `NonexistentToken()`, `NotApproved()` |
| Emits | `Approval(o, to, tokenId)` |

---

### `getApproved(uint256 tokenId) → address`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Reverts | `NonexistentToken()` |

---

### `setApprovalForAll(address operator, bool approved)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Emits | `ApprovalForAll(msg.sender, operator, approved)` |

---

### `isApprovedForAll(address o, address operator) → bool`

| Attribute | Value |
|---|---|
| Visibility | `external view` |

---

### `transferFrom(address from, address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `public` |
| Delegates to | `_transfer(from, to, tokenId)` |

---

### `safeTransferFrom(address from, address to, uint256 tokenId)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Calls | `_transfer`, then `_checkOnERC721Received(from, to, tokenId, "")` |

---

### `safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)`

| Attribute | Value |
|---|---|
| Visibility | `external` |
| Calls | `_transfer`, then `_checkOnERC721Received(from, to, tokenId, data)` |

---

### `_transfer(address from, address to, uint256 tokenId)` (internal)

| Attribute | Value |
|---|---|
| Visibility | `internal` |
| Reverts | `ZeroAddress()`, `NonexistentToken()`, `NotTokenOwner()`, `NotApproved()` |
| State changes | `_ownerOf[tokenId]`, `_balanceOf[from]`, `_balanceOf[to]`, deletes `_approved[tokenId]` |
| Emits | `Transfer(from, to, tokenId)` |

**Description:** Internal transfer implementation. Validates destination, existence, ownership, and authorization. Clears per-token approval. Updates balances and owner in an `unchecked` block (safe within the supply cap). Emits `Transfer`.

---

### `_checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data)` (internal)

| Attribute | Value |
|---|---|
| Visibility | `internal` |
| Reverts | `NotApproved()` if `to` is a contract that doesn't return the correct selector |

**Description:** ERC-721 receiver safety check. Skips for EOAs (`to.code.length == 0`). For contracts, calls `onERC721Received` and validates the return value. Prevents tokens being stranded in contracts not built to hold ERC-721 tokens.

---

### `royaltyInfo(uint256, uint256 salePrice) → (address receiver, uint256 royaltyAmount)`

| Attribute | Value |
|---|---|
| Visibility | `external view` |
| Returns | `receiver = owner`, `royaltyAmount = (salePrice * 500) / 10000` |

**Description:** EIP-2981 royalty lookup. Token ID is ignored — all tokens have the same 5% royalty to `owner`. See Section 2.6 for arithmetic.

---

### `supportsInterface(bytes4 iid) → bool`

| Attribute | Value |
|---|---|
| Visibility | `external pure` |
| Returns | `true` for `0x01ffc9a7` (ERC-165), `0x80ac58cd` (ERC-721), `0x5b5e139f` (ERC-721 Metadata), `0x2a55205a` (ERC-2981) |

---

### `_toString(uint256 v) → string` (internal)

| Attribute | Value |
|---|---|
| Visibility | `internal pure` |
| Returns | Decimal string representation of `v` |

**Description:** Converts `uint256` to its base-10 decimal string. For `v = 3` returns `"3"`. For `v = 0` returns `"0"`. Used only by `tokenURI`.

---

## 2.6 Math

### Mint price

```
PRICE = 0.1 ether = 100_000_000_000_000_000 wei
                  = 1 × 10^17 wei
```

The `ether` suffix in Solidity is a compile-time multiplier: `1 ether = 1_000_000_000_000_000_000 wei` (1 × 10^18). Therefore `0.1 ether = 1 × 10^17` wei = 100,000,000,000,000,000 wei. This is a `uint256` constant embedded in bytecode.

If a caller sends anything other than exactly this amount, `WrongPrice()` reverts. There is no rounding, no tolerance band.

**Total revenue at full sell-out:**
```
5 × 0.1 ETH = 0.5 ETH = 500_000_000_000_000_000 wei
```

### ETH auto-forward in `mint()`

```solidity
(bool ok, ) = owner.call{value: msg.value}("");
```

`msg.value` carries the exact `0.1 ether` (validated above). This call forwards the full amount atomically in the same transaction. No fee is retained by the contract. If the call fails, the whole transaction reverts, meaning no partial state is committed.

### `remaining()` arithmetic

```
remaining = MAX_SUPPLY - totalSupply
          = 5 - totalSupply
```

Simple subtraction. `totalSupply` is bounded to [0, 5] by the `SoldOut` guard, so this result is always in [0, 5] and can never underflow.

### EIP-2981 royalty calculation

```
royaltyAmount = (salePrice × ROYALTY_BPS) / 10_000
              = (salePrice × 500) / 10_000
              = salePrice × 0.05
              = 5% of salePrice
```

**Basis points explained:** 1 basis point = 0.01%. Therefore 500 basis points = 5.00%. The denominator `10_000` represents 100% expressed in basis points.

**Worked example — sale at 1 ETH:**
```
salePrice    = 1_000_000_000_000_000_000 wei (1 ETH)
ROYALTY_BPS  = 500
10_000       = 10_000

royaltyAmount = (1_000_000_000_000_000_000 × 500) / 10_000
              = 500_000_000_000_000_000_000 / 10_000
              = 50_000_000_000_000_000 wei
              = 0.05 ETH
```

**Worked example — sale at 0.5 ETH:**
```
royaltyAmount = (500_000_000_000_000_000 × 500) / 10_000
              = 25_000_000_000_000_000 wei
              = 0.025 ETH
```

**Integer division truncation:** Because Solidity uses integer division, if `salePrice × 500` does not divide evenly by `10_000`, the result truncates (rounds down). For example:

```
salePrice    = 3 wei
royaltyAmount = (3 × 500) / 10_000 = 1500 / 10_000 = 0 wei (truncated)
```

For any `salePrice < 20 wei`, the royalty rounds down to `0`. For any `salePrice ≥ 20 wei`, the royalty is at least `1 wei`. This is standard behavior for on-chain royalty math and does not represent a bug.

**Important:** EIP-2981 is advisory — marketplaces read `royaltyInfo` and are expected to enforce the payment, but the contract itself cannot force marketplaces to honor it. The royalty is not deducted on-chain during NFT transfers; it is a convention adopted by compliant marketplaces (OpenSea, Blur with royalties enabled, etc.).

### Balance arithmetic in `_transfer`

```solidity
unchecked {
    _balanceOf[from] -= 1;
    _balanceOf[to]   += 1;
}
```

`unchecked` removes Solidity's built-in overflow/underflow guard to save gas. Safety argument:
- `_balanceOf[from] -= 1`: Safe because the prior check `o != from` already ensures `from` currently owns the token, and the only way to own a token is to have been minted or transferred one — meaning `_balanceOf[from] >= 1` is guaranteed at this point.
- `_balanceOf[to] += 1`: Safe because there are only 5 tokens total (`MAX_SUPPLY = 5`), so `_balanceOf[to]` can never approach `uint256` max.

### `tokenId = ++totalSupply` in `mint()`

```solidity
unchecked { tokenId = ++totalSupply; }
```

Pre-increment semantics: `totalSupply` is incremented first, then assigned to `tokenId`. First call: `totalSupply` goes from `0` to `1`, `tokenId = 1`. Fifth call: `totalSupply` goes from `4` to `5`, `tokenId = 5`. `unchecked` is safe because `totalSupply` cannot reach 5 when this line runs (the `SoldOut()` check immediately above rejects `totalSupply >= 5`).

---

## 2.7 Events

### `Transfer(address indexed from, address indexed to, uint256 indexed tokenId)`
- **When fired:** Inside `_transfer` on every token movement, and inside `mint()` on each new mint (where `from == address(0)`).
- **What it logs:** Previous holder, new holder, token ID.
- **All three parameters are indexed** — allows efficient log filtering by any combination.

### `Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)`
- **When fired:** Inside `approve()`.
- **What it logs:** Token owner, newly approved address, token ID.

### `ApprovalForAll(address indexed owner, address indexed operator, bool approved)`
- **When fired:** Inside `setApprovalForAll()`.
- **What it logs:** Granting address, operator, and the boolean grant value.

### `Minted(address indexed to, uint256 indexed tokenId)`
- **When fired:** Inside `mint()`, immediately after `Transfer`. Fires only on primary mints — not on secondary transfers.
- **What it logs:** The minter's address and the new token ID.
- **Purpose:** Allows indexers to identify primary mints without the `from == address(0)` heuristic on `Transfer`.

### `Withdraw(address indexed to, uint256 amount)`
- **When fired:** Inside `withdraw()` after a successful ETH push to `owner`.
- **What it logs:** The destination address (`owner`) and the amount sent (in wei).
- **Note:** In normal operation (auto-forwarding on mint), `amount` would be `0` since the contract holds no ETH. This event is meaningful only in edge cases.

---

## 2.8 Security Invariants

1. **Total supply can never exceed 5.** `MAX_SUPPLY` is a compile-time constant. The `>= MAX_SUPPLY` check in `mint()` is the first operation executed, before any state change. Even if `totalSupply` somehow became 5 mid-call (not possible in single-threaded EVM execution), no 6th token would be assigned.

2. **Mint price is permanently 0.1 ETH.** `PRICE` is a compile-time constant. No function can change it. Any call with a different `msg.value` (including zero) reverts.

3. **`owner` is permanently `ghostkey.base.eth`.** Immutable with no setter. Cannot be changed after deployment.

4. **Mint proceeds forward atomically to `owner`.** The ETH forward is the last statement in `mint()` before return, and if it fails, the entire transaction reverts — no token is minted and no ETH is taken from the caller.

5. **No token can be burned.** `_transfer` rejects `to == address(0)` with `ZeroAddress()`. Both `safeTransferFrom` variants delegate to `_transfer`.

6. **Only the token owner or an authorized party can transfer.** Three-way check in `_transfer`: direct owner, per-token approved address, or operator.

7. **Approvals clear on transfer.** `delete _approved[tokenId]` runs before any state update in `_transfer`.

8. **`withdraw` and `setBaseURI` are owner-only.** Both protected by the `onlyOwner` modifier.

9. **Safe transfer to contracts is validated.** `_checkOnERC721Received` prevents tokens being sent to contracts that don't implement the ERC-721 receiver interface.

10. **No reentrancy via mint's ETH forward.** The state updates (token assignment, balance increment) happen before the `owner.call{value:...}` on line 68. Even if `owner` were a malicious contract that reentered `mint()`, `totalSupply` is already incremented — the supply cap check would fail immediately (or a new legitimate token would be minted, consuming the reentrant caller's ETH). In practice, `owner` is a known EOA; this is moot.

---

# 3. Deploy.s.sol

## 3.1 Header

**File:** `script/Deploy.s.sol`
**Purpose:** Foundry deploy script for VaultfireGenesisNFT. Provides the exact `forge script` invocation needed to deploy and verify the Genesis NFT on Base mainnet.

**Note on discrepancy:** The Deploy.s.sol file as committed deploys `VaultfireGenesisNFT` using an older single-argument constructor signature (`new VaultfireGenesisNFT(uri)`), whereas the live contract constructor requires two arguments: `(address recipient_, string memory tokenURI_)`. The actual live deployment was performed via the Arachnid CREATE2 factory with the full two-argument constructor, using the salt `vaultfire-genesis-2026`. This script should be treated as a documentation artifact or a template for future use rather than the exact transcript of the live deployment. The authoritative deployment is verified on Basescan at address `0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C`.

---

## 3.2 Full Source Code

```solidity
     1	// SPDX-License-Identifier: MIT
     2	pragma solidity 0.8.25;
     3	
     4	import "forge-std/Script.sol";
     5	import "../contracts/VaultfireGenesisNFT.sol";
     6	
     7	/// @notice Deploys VaultfireGenesisNFT to Base and auto-mints token #1 to deployer.
     8	/// Run:
     9	///   forge script script/Deploy.s.sol \
    10	///     --rpc-url base \
    11	///     --broadcast \
    12	///     --verify \
    13	///     --etherscan-api-key $BASESCAN_KEY \
    14	///     --account deployer
    15	contract Deploy is Script {
    16	    function run() external {
    17	        string memory uri = "https://theloopbreaker.com/nft/vaultfire-genesis.json";
    18	        vm.startBroadcast();
    19	        VaultfireGenesisNFT nft = new VaultfireGenesisNFT(uri);
    20	        vm.stopBroadcast();
    21	        console.log("VaultfireGenesisNFT deployed:", address(nft));
    22	        console.log("Token #1 owner:", nft.ownerOf(1));
    23	        console.log("Token URI:", nft.tokenURI(1));
    24	    }
    25	}
```

---

## 3.3 Line-by-Line Walkthrough

**Lines 1–2 — License and compiler version**
Standard header. Matches the contract files.

**Lines 4–5 — Imports**
- `forge-std/Script.sol` — provides the `Script` base contract and the `vm` cheatcode object, used to control broadcast (which transactions get signed and sent).
- `../contracts/VaultfireGenesisNFT.sol` — imports the contract being deployed.

**Lines 7–14 — NatSpec block with embedded run command**
Documents the intended usage. The `forge script` invocation:
- `--rpc-url base` — uses the `base` network alias from `foundry.toml`'s `[rpc_endpoints]`.
- `--broadcast` — actually submits transactions (without this, forge only simulates).
- `--verify` — submits source code for Basescan verification after deployment.
- `--etherscan-api-key $BASESCAN_KEY` — uses the `BASESCAN_KEY` environment variable for Basescan API access.
- `--account deployer` — uses the `deployer` keystore account (managed by `cast wallet`).

**Line 15 — `contract Deploy is Script`**
The deploy script is itself a Solidity contract. It inherits `Script` from forge-std, which provides access to `vm.*` cheatcodes (available only in the Foundry testing/scripting environment, not on-chain). `console.log` is also provided by forge-std.

**Line 16 — `function run() external`**
The entry point Foundry calls when running the script. By convention, Foundry scripts expose a `run()` function.

**Line 17 — `string memory uri = ...`**
Stores the metadata URL as a local string. This is the URL passed to the constructor as `tokenURI_`. In the actual live deployment, this URL was passed alongside the `recipient_` address.

**Lines 18–20 — `vm.startBroadcast()` / `vm.stopBroadcast()`**
The `vm.startBroadcast()` / `vm.stopBroadcast()` pair defines the transaction boundary. Any state-changing calls between these two lines are signed by the deployer's key and broadcast to the network. The `new VaultfireGenesisNFT(uri)` on line 19 is the only such call — it creates the contract in one transaction.

**Line 19 — `new VaultfireGenesisNFT(uri)`**
Deploys the contract using a standard `new` call (not CREATE2). This generates a standard CREATE address derived from `deployer + nonce`, not the deterministic CREATE2 address. The live contract at `0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C` was deployed via the Arachnid CREATE2 factory — see Section 4 for the exact recipe.

**Lines 21–23 — `console.log` statements**
Print deployment diagnostics to the Foundry console (not on-chain). Confirms the deployed address, the owner of token #1 (who receives it in the constructor), and the token URI. These are sanity checks for the deployer.

---

# 4. Rebuild Recipe

This section provides the exact commands a developer would run to reproduce the live contracts from source — from environment setup through deterministic CREATE2 deployment.

## 4.1 Prerequisites

```bash
# Install Foundry (includes forge, cast, anvil)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# Verify forge version (must support solc 0.8.25)
forge --version

# Install solc 0.8.25 via svm (Foundry manages this automatically)
# Or install manually:
# https://github.com/ethereum/solc-bin/
```

## 4.2 Project Setup

```bash
mkdir vaultfire-genesis-nft
cd vaultfire-genesis-nft
forge init --no-commit

# Place contracts at:
# contracts/VaultfireGenesisNFT.sol
# contracts/VaultfireFoundersEdition.sol
# script/Deploy.s.sol
```

## 4.3 foundry.toml Configuration

```toml
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
solc_version = "0.8.25"
optimizer = true
optimizer_runs = 200
via_ir = true
evm_version = "cancun"

[rpc_endpoints]
base = "https://mainnet.base.org"
# Or use: base = "${BASE_RPC_URL}"

[etherscan]
base = { key = "${BASESCAN_KEY}", url = "https://api.basescan.org/api" }
```

## 4.4 Compile

```bash
forge build
# Expects: out/VaultfireGenesisNFT.sol/VaultfireGenesisNFT.json
#          out/VaultfireFoundersEdition.sol/VaultfireFoundersEdition.json
```

Verify compiler settings match the live deployment:
```bash
# solc version
forge config --profile default | grep solc_version
# → 0.8.25

# Check optimizer settings in foundry.toml:
# optimizer = true, optimizer_runs = 200, via_ir = true, evm_version = "cancun"
```

## 4.5 Encode Constructor Arguments

The Arachnid CREATE2 factory deploys contracts by prepending the constructor bytecode. You need to ABI-encode the constructor arguments and append them to the init bytecode.

**For VaultfireGenesisNFT:**

```bash
# Constructor: (address recipient_, string memory tokenURI_)
# recipient_  = 0xfA15Ee28939B222B0448261A22156070f0A7813C (ghostkey.base.eth)
# tokenURI_   = "https://theloopbreaker.com/nft/vaultfire-genesis.json"

cast abi-encode "constructor(address,string)" \
  0xfA15Ee28939B222B0448261A22156070f0A7813C \
  "https://theloopbreaker.com/nft/vaultfire-genesis.json"
```

**For VaultfireFoundersEdition:**

```bash
# Constructor: (address owner_, string memory baseURI_)
# owner_    = 0xfA15Ee28939B222B0448261A22156070f0A7813C (ghostkey.base.eth)
# baseURI_  = "https://theloopbreaker.com/nft/founders/"

cast abi-encode "constructor(address,string)" \
  0xfA15Ee28939B222B0448261A22156070f0A7813C \
  "https://theloopbreaker.com/nft/founders/"
```

## 4.6 Compute Deterministic Addresses (Verify Before Deploy)

The Arachnid CREATE2 factory address: `0x4e59b44847b379578588920ca78fbf26c0b4956c`

The CREATE2 address formula:
```
address = keccak256(0xff ‖ factory ‖ salt ‖ keccak256(initcode))[12:]
```
where `initcode = compiledBytecode ‖ abiEncodedConstructorArgs`.

```bash
# Genesis salt (bytes32)
GENESIS_SALT=0x7661756c74666972652d67656e657369732d3230323600000000000000000000

# Founders salt (bytes32)
FOUNDERS_SALT=0x7661756c74666972652d666f756e646572732d32303236000000000000000000

# Verify salt strings (ASCII decode):
# vaultfire-genesis-2026  → 0x7661756c74666972652d67656e657369732d323032360000...
# vaultfire-founders-2026 → 0x7661756c74666972652d666f756e646572732d323032360000...

python3 -c "print(bytes.fromhex('7661756c74666972652d67656e657369732d323032360000000000000000000000000000').decode())"
# → vaultfire-genesis-2026

python3 -c "print(bytes.fromhex('7661756c74666972652d666f756e646572732d3230323600000000000000000000').decode())"
# → vaultfire-founders-2026
```

Use `cast create2` to compute the expected address:

```bash
# Genesis
GENESIS_INITCODE=$(cat out/VaultfireGenesisNFT.sol/VaultfireGenesisNFT.json | jq -r '.bytecode.object')
GENESIS_ARGS=$(cast abi-encode "constructor(address,string)" \
  0xfA15Ee28939B222B0448261A22156070f0A7813C \
  "https://theloopbreaker.com/nft/vaultfire-genesis.json")
GENESIS_FULL_INITCODE="${GENESIS_INITCODE}${GENESIS_ARGS:2}"

cast compute-address \
  --factory 0x4e59b44847b379578588920ca78fbf26c0b4956c \
  --salt $GENESIS_SALT \
  --init-code-hash $(cast keccak $GENESIS_FULL_INITCODE)
# Expected: 0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C
```

Repeat analogously for Founders. If the computed address matches the live address, bytecode is deterministically reproducible.

## 4.7 Deploy via Arachnid CREATE2 Factory

The Arachnid factory is at `0x4e59b44847b379578588920ca78fbf26c0b4956c` on Base mainnet. It accepts a single `bytes` calldata argument: the salt (first 32 bytes) followed by the init bytecode.

```bash
# Deploy VaultfireGenesisNFT
export PRIVATE_KEY=<your-key-or-use-cast-wallet>
export RPC_URL=https://mainnet.base.org

GENESIS_INITCODE=$(cat out/VaultfireGenesisNFT.sol/VaultfireGenesisNFT.json | jq -r '.bytecode.object')
GENESIS_ARGS=$(cast abi-encode "constructor(address,string)" \
  0xfA15Ee28939B222B0448261A22156070f0A7813C \
  "https://theloopbreaker.com/nft/vaultfire-genesis.json")
GENESIS_PAYLOAD="${GENESIS_SALT}${GENESIS_INITCODE:2}${GENESIS_ARGS:2}"

cast send \
  --rpc-url $RPC_URL \
  --private-key $PRIVATE_KEY \
  0x4e59b44847b379578588920ca78fbf26c0b4956c \
  $GENESIS_PAYLOAD
```

```bash
# Deploy VaultfireFoundersEdition
FOUNDERS_INITCODE=$(cat out/VaultfireFoundersEdition.sol/VaultfireFoundersEdition.json | jq -r '.bytecode.object')
FOUNDERS_ARGS=$(cast abi-encode "constructor(address,string)" \
  0xfA15Ee28939B222B0448261A22156070f0A7813C \
  "https://theloopbreaker.com/nft/founders/")
FOUNDERS_PAYLOAD="${FOUNDERS_SALT}${FOUNDERS_INITCODE:2}${FOUNDERS_ARGS:2}"

cast send \
  --rpc-url $RPC_URL \
  --private-key $PRIVATE_KEY \
  0x4e59b44847b379578588920ca78fbf26c0b4956c \
  $FOUNDERS_PAYLOAD
```

## 4.8 Verify on Basescan

```bash
# Verify VaultfireGenesisNFT
forge verify-contract \
  0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C \
  contracts/VaultfireGenesisNFT.sol:VaultfireGenesisNFT \
  --chain-id 8453 \
  --compiler-version 0.8.25 \
  --num-of-optimizations 200 \
  --via-ir \
  --constructor-args $(cast abi-encode "constructor(address,string)" \
    0xfA15Ee28939B222B0448261A22156070f0A7813C \
    "https://theloopbreaker.com/nft/vaultfire-genesis.json") \
  --etherscan-api-key $BASESCAN_KEY \
  --watch
```

```bash
# Verify VaultfireFoundersEdition
forge verify-contract \
  0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE \
  contracts/VaultfireFoundersEdition.sol:VaultfireFoundersEdition \
  --chain-id 8453 \
  --compiler-version 0.8.25 \
  --num-of-optimizations 200 \
  --via-ir \
  --constructor-args $(cast abi-encode "constructor(address,string)" \
    0xfA15Ee28939B222B0448261A22156070f0A7813C \
    "https://theloopbreaker.com/nft/founders/") \
  --etherscan-api-key $BASESCAN_KEY \
  --watch
```

## 4.9 Post-Deploy Verification

```bash
# Confirm Genesis token #1 owner
cast call 0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C \
  "ownerOf(uint256)(address)" 1 \
  --rpc-url $RPC_URL
# Expected: 0xfA15Ee28939B222B0448261A22156070f0A7813C

# Confirm Genesis token URI
cast call 0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C \
  "tokenURI(uint256)(string)" 1 \
  --rpc-url $RPC_URL
# Expected: https://theloopbreaker.com/nft/vaultfire-genesis.json

# Confirm Founders supply (0 before any mint, up to 5)
cast call 0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE \
  "totalSupply()(uint256)" \
  --rpc-url $RPC_URL

# Confirm Founders price
cast call 0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE \
  "PRICE()(uint256)" \
  --rpc-url $RPC_URL
# Expected: 100000000000000000

# Confirm Founders royalty (sale of 1 ETH)
cast call 0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE \
  "royaltyInfo(uint256,uint256)(address,uint256)" 1 1000000000000000000 \
  --rpc-url $RPC_URL
# Expected: (0xfA15Ee28939B222B0448261A22156070f0A7813C, 50000000000000000)
# i.e. ghostkey.base.eth receives 0.05 ETH on a 1 ETH sale

# Confirm both contracts support ERC-2981 (Founders only)
cast call 0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE \
  "supportsInterface(bytes4)(bool)" 0x2a55205a \
  --rpc-url $RPC_URL
# Expected: true

# Confirm Genesis does NOT declare ERC-2981
cast call 0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C \
  "supportsInterface(bytes4)(bool)" 0x2a55205a \
  --rpc-url $RPC_URL
# Expected: false
```

---

## 4.10 Summary of Live Deployment Facts

| Parameter | Genesis | Founders |
|---|---|---|
| Contract name | VaultfireGenesisNFT | VaultfireFoundersEdition |
| Symbol | VFGEN | VFFE |
| Chain | Base mainnet (8453) | Base mainnet (8453) |
| Address | `0xdC5fE0da17864608eDC3c24A675E6C4C24a9832C` | `0x1bcD9B7Cad06D2BbC7FdedC46B32eFd1988e28cE` |
| Owner | `0xfA15Ee28939B222B0448261A22156070f0A7813C` (ghostkey.base.eth) | `0xfA15Ee28939B222B0448261A22156070f0A7813C` (ghostkey.base.eth) |
| Deploy factory | `0x4e59b44847b379578588920ca78fbf26c0b4956c` (Arachnid CREATE2) | `0x4e59b44847b379578588920ca78fbf26c0b4956c` (Arachnid CREATE2) |
| Salt (bytes32) | `0x7661756c74666972652d67656e657369732d3230323600000000000000000000` | `0x7661756c74666972652d666f756e646572732d32303236000000000000000000` |
| Salt (ASCII) | `vaultfire-genesis-2026` | `vaultfire-founders-2026` |
| Supply | 1/1 (already minted) | 0–5 (public mint) |
| Mint price | None (no public mint) | 0.1 ETH exactly |
| Token #1 owner | ghostkey.base.eth | First caller of mint() |
| Royalties | None (no ERC-2981) | 5% (500 bps) to ghostkey.base.eth |
| Proceeds | N/A | Auto-forwarded to ghostkey.base.eth on every mint |
| Compiler | solc 0.8.25 | solc 0.8.25 |
| Optimizer runs | 200 | 200 |
| via-ir | Yes | Yes |
| EVM target | cancun | cancun |
| Dependencies | None | None |
| Standards | ERC-165, ERC-721, ERC-721 Metadata | ERC-165, ERC-721, ERC-721 Metadata, ERC-2981 |

---

*End of Vaultfire Live NFT Contracts — Technical Reference*
*Codex Batch 1 · Generated from source at /home/user/workspace/vaultfire-genesis-nft/*


<div style="page-break-before:always"></div>

# Vaultfire Codex — Batch 2: V3 Bond Contracts

**Output file:** `/home/user/workspace/vaultfire-codex/02_V3_BONDS.md`
**Manifest:** `/tmp/codex/2_v3_bonds_manifest.txt`
**Contracts documented:** 25 unique content-hashed files covering 7 distinct contract families

---

## Table of Contents

1. [BaseDignityBond](#contract-1-basedignitybond)
2. [BaseYieldPoolBond (V2 — theloopbreaker-site)](#contract-2-baseyieldpoolbond-v2-theloopbreaker-site)
3. [BaseYieldPoolBond (V2 — root workspace)](#contract-3-baseyieldpoolbond-v2-root-workspace)
4. [BaseYieldPoolBondV3 (push_contracts / V3.1 pre-audit)](#contract-4-baseyieldpoolbondv3-v31-pre-audit)
5. [BaseYieldPoolBondV3 (vf-audit-contracts / V3.3 post-audit)](#contract-5-baseyieldpoolbondv3-v33-post-audit)
6. [BaseYieldPoolBondV3 (v3 and v3-pr-ready variants)](#contract-6-baseyieldpoolbondv3-v3-and-v3-pr-ready-variants)
7. [AIAccountabilityBondsV2](#contract-7-aiaccountabilitybondsv2)
8. [AIAccountabilityBondsV2 (theloopbreaker-site variant)](#contract-8-aiaccountabilitybondsv2-theloopbreaker-site-variant)
9. [AIAccountabilityBondsV3 (root workspace / vf-audit-contracts — V3.3 final)](#contract-9-aiaccountabilitybondsv3-v33-final)
10. [AIAccountabilityBondsV3 (push_contracts — V3.1)](#contract-10-aiaccountabilitybondsv3-push-v31)
11. [AIAccountabilityBondsV3 (v3 directory)](#contract-11-aiaccountabilitybondsv3-v3-directory)
12. [AIAccountabilityBondsV3 (v3-pr-ready)](#contract-12-aiaccountabilitybondsv3-v3-pr-ready)
13. [AIPartnershipBondsV2 (root workspace)](#contract-13-aipartnershipbondsv2-root)
14. [AIPartnershipBondsV2 (theloopbreaker-site)](#contract-14-aipartnershipbondsv2-theloopbreaker-site)
15. [AIPartnershipBondsV3 (AIPartnershipBondsV3_to_push — V3.1 pre-audit)](#contract-15-aipartnershipbondsv3-to-push)
16. [AIPartnershipBondsV3 (push_contracts — V3.1 push candidate)](#contract-16-aipartnershipbondsv3-push-contracts)
17. [AIPartnershipBondsV3 (vf-audit-contracts — V3.3 final)](#contract-17-aipartnershipbondsv3-vf-audit-v33)
18. [AIPartnershipBondsV3 (v3 directory)](#contract-18-aipartnershipbondsv3-v3-directory)
19. [AIPartnershipBondsV3 (v3-pr-ready)](#contract-19-aipartnershipbondsv3-v3-pr-ready)
20. [AIPartnershipBondsV3 (test_fixes)](#contract-20-aipartnershipbondsv3-test-fixes)
21. [VaultfireBondInsurancePool](#contract-21-vaultfirebondinsurancepool)
22. [VaultfireBIPVault (ERC-4626 view adapter)](#contract-22-vaultfirebipvault)
23. [VaultfireBIPVault.t.sol (Foundry test)](#contract-23-vaultfirebipvault-test)
24. [PartnershipBondsEnhanced](#contract-24-partnershipbondsenhanced)
25. [Version-Lineage Summary and Deployment Guide](#section-25-version-lineage-and-deployment-guide)

---

## Manifest Deduplication Notes

The 25 paths listed in `2_v3_bonds_manifest.txt` resolve to **7 distinct contract families** across **multiple version generations**. Content-hash comparison (md5) reveals:

| Family | Distinct content hashes found |
|---|---|
| BaseYieldPoolBond | 6 distinct files (V2 root, V2 loopbreaker, V3.1 push, V3.1 v3-dir, V3.1 v3-pr-ready, V3.3 vf-audit) |
| AIAccountabilityBonds | 5 distinct files (V2 root, V2 loopbreaker, V3.1 push, V3.1 v3-dir/v3-pr-ready, V3.3 root/vf-audit) |
| AIPartnershipBonds | 6 distinct files |
| BaseDignityBond | 1 |
| VaultfireBondInsurancePool | 1 |
| VaultfireBIPVault (src + test) | 2 |
| PartnershipBondsEnhanced | 1 |

Where two paths share an identical content hash, the second entry references the primary documentation and notes the duplication.

---

# Contract 1: BaseDignityBond

**Filename:** `BaseDignityBond.sol`
**Path:** `./BaseDignityBond.sol`
**Version:** V2 / foundational base (unchanged across V2 and V3 bond stacks)
**Role:** Abstract base contract for all Universal Dignity Bond contracts. Provides emergency pausability, two-step ownership transfer, and common input-validation helpers inherited by every bond type.

## Full Source Code

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.25;
     3
     4  import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
     5  import "@openzeppelin/contracts/utils/Pausable.sol";
     6
     7  /**
     8   * @title BaseDignityBond
     9   * @notice Base contract for all Universal Dignity Bonds
    10   * @dev Provides common functionality: pausability, ownership, score validation
    11   *
    12   * Mission Alignment: Emergency protection for workers/communities.
    13   * Can pause if exploit detected, protecting real people from harm.
    14   */
    15  abstract contract BaseDignityBond is ReentrancyGuard, Pausable {
    16
    17      // ============ State Variables ============
    18
    19      /// @notice Contract owner (can pause in emergency)
    20      address public owner;
    21
    22      /// @notice Pending owner for two-step ownership transfer
    23      /// @dev Prevents accidental ownership loss from typos in address
    24      address public pendingOwner;
    25
    26      /// @notice Timelock period for distributions (7 days)
    27      uint256 public constant DISTRIBUTION_TIMELOCK = 7 days;
    28
    29      // ============ Events ============
    30
    31      /// @notice Emitted when contract is paused
    32      event ContractPaused(address indexed by, uint256 timestamp);
    33
    34      /// @notice Emitted when contract is unpaused
    35      event ContractUnpaused(address indexed by, uint256 timestamp);
    36
    37      /// @notice Emitted when ownership transfer is initiated
    38      event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);
    39
    40      /// @notice Emitted when ownership is transferred
    41      event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    42
    43      // ============ Constructor ============
    44
    45      constructor() {
    46          owner = msg.sender;
    47      }
    48
    49      // ============ Modifiers ============
    50
    51      /**
    52       * @notice Restricts function to contract owner
    53       */
    54      modifier onlyOwner() {
    55          require(msg.sender == owner, "Only owner can call");
    56          _;
    57      }
    58
    59      // ============ Owner Functions ============
    60
    61      function pause() external onlyOwner {
    62          _pause();
    63          emit ContractPaused(msg.sender, block.timestamp);
    64      }
    65
    66      function unpause() external onlyOwner {
    67          _unpause();
    68          emit ContractUnpaused(msg.sender, block.timestamp);
    69      }
    70
    71      function transferOwnership(address newOwner) external onlyOwner {
    72          require(newOwner != address(0), "New owner cannot be zero address");
    79          require(newOwner != owner, "Already the owner");
    80          pendingOwner = newOwner;
    81          emit OwnershipTransferStarted(owner, newOwner);
    82      }
    83
    84      function acceptOwnership() external {
    85          require(msg.sender == pendingOwner, "Only pending owner can accept");
    86          require(pendingOwner != address(0), "No pending ownership transfer");
    87          address previousOwner = owner;
    88          owner = pendingOwner;
    89          pendingOwner = address(0);
    90          emit OwnershipTransferred(previousOwner, owner);
    91      }
    92
    93      function cancelOwnershipTransfer() external onlyOwner {
    94          require(pendingOwner != address(0), "No pending transfer to cancel");
    95          pendingOwner = address(0);
    96          emit OwnershipTransferStarted(owner, address(0));
    97      }
    98
    99      function _validateScore(uint256 score, string memory paramName) internal pure {
   100          require(score <= 10000, string(abi.encodePacked(paramName, " must be 0-10000")));
   101      }
   102
   103      function _validateScores(uint256[] memory scores) internal pure {
   104          uint256 length = scores.length;
   105          for (uint256 i = 0; i < length;) {
   106              require(scores[i] <= 10000, "Score must be 0-10000");
   107              unchecked { ++i; }
   108          }
   109      }
   110
   111      function _validateAddress(address addr, string memory paramName) internal pure {
   112          require(addr != address(0),
   113              string(abi.encodePacked(paramName, " cannot be zero address")));
   114      }
   115
   116      function _validateNonZero(uint256 amount, string memory paramName) internal pure {
   117          require(amount > 0,
   118              string(abi.encodePacked(paramName, " must be greater than zero")));
   117      }
   118
   119      function isPaused() external view returns (bool) {
   120          return paused();
   121      }
   122  }
```

## Line-by-Line Walkthrough

**Lines 1–2** — License and compiler pragma. `^0.8.25` means any 0.8.x compiler from 0.8.25 upward. Solidity 0.8 has checked arithmetic by default, so overflow and underflow revert automatically without `SafeMath`.

**Lines 4–5** — Two OpenZeppelin imports. `ReentrancyGuard` provides the `nonReentrant` modifier used by every money-handling function in the chain. `Pausable` provides `_pause()`, `_unpause()`, and the `whenNotPaused` modifier that gates bond creation and distribution.

**Line 15** — `abstract contract BaseDignityBond is ReentrancyGuard, Pausable`. Marked `abstract` because it defines no concrete bond logic — it is always inherited. Multiple inheritance via C3 linearization: `ReentrancyGuard` is leftmost, `Pausable` rightmost.

**Lines 20–24** — `owner` is the deployer by default (set in constructor). `pendingOwner` supports a two-step transfer. Both are `public`, so any caller can read them — important for governance transparency.

**Line 27** — `DISTRIBUTION_TIMELOCK = 7 days`. Evaluated at compile-time as `7 * 86400 = 604800` seconds. Stored as an `immutable`-like constant (no SLOAD, embedded in bytecode). All child bond distribution timelocks inherit this value.

**Lines 45–47** — Constructor sets `owner = msg.sender`. Because this is `abstract`, the constructor is called via the most-derived child contract's `super` chain, so `msg.sender` is whoever deploys the final concrete contract.

**Lines 54–57** — `onlyOwner` modifier. Single require that checks `msg.sender == owner`. No role-based access control — intentionally simple to minimize attack surface.

**Lines 61–63** — `pause()`. Calls OZ's `_pause()` (which flips an internal bool in the `Pausable` storage slot) and emits `ContractPaused`. After this call, any function marked `whenNotPaused` reverts.

**Lines 66–68** — `unpause()`. Mirror of `pause()`.

**Lines 71–81** — `transferOwnership()`. Step 1 of 2. Does NOT immediately change `owner`. Writes `pendingOwner = newOwner` and emits `OwnershipTransferStarted`. The two guards (`!= address(0)` and `!= owner`) prevent trivial mistakes. This fixes the HIGH-005 audit finding present in very early versions.

**Lines 84–91** — `acceptOwnership()`. Step 2 of 2. Only callable by `pendingOwner`. Writes `owner = pendingOwner`, clears `pendingOwner`, emits `OwnershipTransferred`. If the pending owner address was a typo this call will simply be uncallable, leaving the original owner in control.

**Lines 93–97** — `cancelOwnershipTransfer()`. Only current owner can cancel. Sets `pendingOwner = address(0)`. Emits `OwnershipTransferStarted(owner, address(0))` as a signal; strictly speaking this reuses the "started" event to communicate cancellation — a minor UX inconsistency but not a security issue.

**Lines 99–101** — `_validateScore()`. Pure function (no SLOAD). Accepts a score and a param name string and asserts `score <= 10000`. The bound of 10000 is the universal basis-point scale used throughout Vaultfire (0 = worst, 10000 = best).

**Lines 103–109** — `_validateScores()`. Batch version. Uses unchecked `++i` for the loop counter to save gas (overflow of a loop counter is not realistic since arrays are memory-bound).

**Lines 111–114** — `_validateAddress()`. Ensures no zero-address is passed for addresses that need to receive funds or be called.

**Lines 116–119** — `_validateNonZero()`. Ensures amounts are positive.

**Lines 119–121** — `isPaused()`. Thin wrapper over OZ `paused()`. Useful for off-chain monitoring and UI.

## State Variables

| Name | Type | Mutability | Who Can Change | What It Stores |
|---|---|---|---|---|
| `owner` | `address` | Mutable | `acceptOwnership()` (pending owner) | Current contract owner |
| `pendingOwner` | `address` | Mutable | `transferOwnership()` (owner), `cancelOwnershipTransfer()` (owner), `acceptOwnership()` (pending owner) | Proposed next owner awaiting confirmation |
| `DISTRIBUTION_TIMELOCK` | `uint256 constant` | Immutable | Nobody | 604800 seconds (7 days) — global timelock for all bond distributions |

*Inherited from ReentrancyGuard:* `_status` (uint256 private, 1 = not entered, 2 = entered)
*Inherited from Pausable:* `_paused` (bool private)

## Functions

### `pause()`
- **Signature:** `function pause() external onlyOwner`
- **Visibility:** external
- **Modifiers:** `onlyOwner`
- **What it does:** Sets OZ Pausable's internal `_paused = true`. All `whenNotPaused`-gated functions now revert.
- **Who can call:** owner
- **What it changes:** `_paused` (via OZ)
- **Returns:** nothing
- **Reverts:** if `msg.sender != owner`

### `unpause()`
- **Signature:** `function unpause() external onlyOwner`
- **Visibility:** external
- **Modifiers:** `onlyOwner`
- **What it does:** Sets `_paused = false`. Normal operations resume.
- **Who can call:** owner
- **Reverts:** if `msg.sender != owner`

### `transferOwnership(address newOwner)`
- **Signature:** `function transferOwnership(address newOwner) external onlyOwner`
- **Visibility:** external
- **Modifiers:** `onlyOwner`
- **What it does:** Initiates ownership transfer. Does NOT change `owner` yet. Writes `pendingOwner`.
- **Who can call:** owner
- **Reverts:** `newOwner == address(0)`, `newOwner == owner`

### `acceptOwnership()`
- **Signature:** `function acceptOwnership() external`
- **Visibility:** external
- **No modifiers** (open — anyone can call, but only passes if they ARE the pending owner)
- **What it does:** Completes the transfer. Sets `owner = pendingOwner`, clears `pendingOwner`.
- **Who can call:** only address stored in `pendingOwner`
- **Reverts:** `msg.sender != pendingOwner`, `pendingOwner == address(0)`

### `cancelOwnershipTransfer()`
- **Signature:** `function cancelOwnershipTransfer() external onlyOwner`
- **Visibility:** external
- **Modifiers:** `onlyOwner`
- **What it does:** Aborts a pending transfer. Clears `pendingOwner`.
- **Reverts:** `pendingOwner == address(0)` (no transfer in flight)

### `_validateScore(uint256 score, string memory paramName)` (internal pure)
### `_validateScores(uint256[] memory scores)` (internal pure)
### `_validateAddress(address addr, string memory paramName)` (internal pure)
### `_validateNonZero(uint256 amount, string memory paramName)` (internal pure)
### `isPaused()` (external view)

All validation helpers are `pure` (no storage reads) except `isPaused()` which is `view`.

## Events

| Event | When Fired | Parameters |
|---|---|---|
| `ContractPaused` | `pause()` called | `by` (indexed address), `timestamp` |
| `ContractUnpaused` | `unpause()` called | `by`, `timestamp` |
| `OwnershipTransferStarted` | `transferOwnership()` or `cancelOwnershipTransfer()` | `previousOwner` (indexed), `newOwner` (indexed) |
| `OwnershipTransferred` | `acceptOwnership()` | `previousOwner` (indexed), `newOwner` (indexed) |

## Security Invariants

- **Owner can never be the zero address** once deployed (constructor sets `owner = msg.sender`, transfers require non-zero destination).
- **Bond operations cannot occur while paused** — all child-contract `createBond`, `distributeBond` etc. are gated by `whenNotPaused`.
- **Two-step ownership prevents one-TX loss** — a wrong address in `transferOwnership` can always be cancelled before `acceptOwnership` is called.
- **Score values are always in [0, 10000]** — `_validateScore` enforces this on every metrics submission.

## Dependencies

| Import | Used For |
|---|---|
| `@openzeppelin/contracts/utils/ReentrancyGuard.sol` | `nonReentrant` modifier on all fund-transferring functions |
| `@openzeppelin/contracts/utils/Pausable.sol` | `_pause()`, `_unpause()`, `whenNotPaused`, `paused()` |

## Rebuild Recipe

- **Solc:** `^0.8.25` (compile with 0.8.25 or later)
- **Optimizer:** 200 runs recommended (standard Vaultfire setting)
- **Constructor args:** none (abstract — never deployed standalone)
- **Deployment order:** Deployed as part of inheriting concrete contract; no standalone deployment

---

# Contract 2: BaseYieldPoolBond (V2 — theloopbreaker-site)

**Filename:** `BaseYieldPoolBond.sol`
**Path:** `./theloopbreaker-site/contracts/BaseYieldPoolBond.sol`
**Content hash:** `3c68d22a...` (distinct from root workspace copy)
**Version:** V2 — production-ready yield pool base (theloopbreaker-site branch)

This file is essentially identical in logic to the root workspace copy (Contract 3 below) with only minor whitespace and comment differences. Documentation focuses on the root workspace canonical version; see Contract 3.

---

# Contract 3: BaseYieldPoolBond (V2 — root workspace)

**Filename:** `BaseYieldPoolBond.sol`
**Path:** `./BaseYieldPoolBond.sol`
**Content hash:** `972e4387...`
**Version:** V2

## What It Does

`BaseYieldPoolBond` is the abstract V2 base layer that sits between `BaseDignityBond` and the concrete bond contracts (`AIAccountabilityBondsV2`, `AIPartnershipBondsV2`). Its primary responsibility is maintaining a native-token yield pool — a reserve fund from which bond appreciations are paid out. It implements insolvency-prevention logic: before any distribution, the contract verifies the pool can cover both current and all pending distribution obligations. It also tracks total active bond value so a health ratio can be computed on demand.

## Full Source Code

(314 lines — see file at `/home/user/workspace/BaseYieldPoolBond.sol`. Reproduced with line numbers below.)

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.25;
     3
     4  import "./BaseDignityBond.sol";
     5
     6  /**
     7   * @title BaseYieldPoolBond
     8   * @notice Base contract for bonds with yield pool funding mechanism
     9   * ...
    10   */
    11  abstract contract BaseYieldPoolBond is BaseDignityBond {
    12
    13      // ============ State Variables ============
    14
    15      /// @notice Pool of funds available for bond appreciations
    16      uint256 public yieldPool;
    17
    18      /// @notice Minimum yield pool balance required
    19      uint256 public minimumYieldPoolBalance;
    20
    21      /// @notice Total value of all active bonds (for reserve ratio calculation)
    22      uint256 public totalActiveBondValue;
    23
    24      /// @notice Whether yield pool checks are enforced
    25      bool public yieldPoolEnforced;
    26
    27      /// @notice Total pending distribution amounts across all bonds
    28      uint256 public totalPendingDistributions;
    29
    30      /// @notice Pending distribution amount for each bond
    31      mapping(uint256 => uint256) public pendingDistributionAmounts;
    32
    33      /// @notice Snapshotted appreciation at distribution request time
    34      mapping(uint256 => int256) public snapshotAppreciation;
    35
    36      // ============ Constants ============
    37
    38      uint256 public constant DEFAULT_MINIMUM_YIELD_POOL = 10 ether;
    39      uint256 public constant MINIMUM_RESERVE_RATIO = 5000;
    40
    41      // ============ Events ============
    42
    43      event YieldPoolFunded(address indexed funder, uint256 amount, uint256 newBalance);
    44      event YieldPoolWithdrawn(address indexed owner, uint256 amount, uint256 newBalance);
    45      event YieldPoolUsed(uint256 indexed bondId, uint256 amount, uint256 remainingBalance);
    46      event YieldPoolReplenished(uint256 indexed bondId, uint256 amount, uint256 newBalance);
    47      event MinimumYieldPoolUpdated(uint256 oldMinimum, uint256 newMinimum);
    48      event YieldPoolEnforcementChanged(bool enforced);
    49      event LowYieldPoolWarning(uint256 currentBalance, uint256 minimumRequired, uint256 deficit);
    50      event ReserveRatioWarning(uint256 yieldPool, uint256 activeBonds, uint256 ratio);
    51
    52      // ============ Constructor ============
    53
    54      constructor() {
    55          minimumYieldPoolBalance = DEFAULT_MINIMUM_YIELD_POOL;
    56          yieldPoolEnforced = true;
    57      }
    58
    59      // ...
    60  }
```

## Line-by-Line Walkthrough

**Line 4** — Imports `BaseDignityBond.sol`, pulling in OpenZeppelin `ReentrancyGuard` and `Pausable` through the inheritance chain.

**Line 11** — `abstract contract BaseYieldPoolBond is BaseDignityBond`. Inherits all owner/pause/validation machinery. Still abstract — no bond-specific logic.

**Line 16** — `uint256 public yieldPool`. The key accounting variable. Tracks the total native ETH (or AVAX) held in the contract that is earmarked for distribution appreciations. This is NOT the contract's full balance — it excludes staked principal from individual bonds and any accumulated protocol funds. It is increased by `fundYieldPool()` calls and `_replenishYieldPool()` (on depreciation), and decreased by `_useYieldPool()` (on distribution pay-outs).

**Line 19** — `minimumYieldPoolBalance`. A floor below which `withdrawYieldPool` cannot drain the pool, and (when `yieldPoolEnforced = true`) below which distributions are paused. Default = `DEFAULT_MINIMUM_YIELD_POOL` = 10 ether. Owner can adjust via `setMinimumYieldPoolBalance`.

**Line 22** — `totalActiveBondValue`. Running sum of all currently active bond stake amounts, in wei. Used only for the reserve ratio calculation (`getReserveRatio`). Updated by child contracts whenever a bond is created or deactivated.

**Line 25** — `yieldPoolEnforced`. Boolean gate. When `true`, the `minimumYieldPoolBalance` floor is enforced during distribution (extra safety check beyond the basic coverage check). Owner can disable temporarily in governance emergencies.

**Line 28** — `totalPendingDistributions`. Sum of all appreciated amounts that have been committed (via `requestDistribution`) but not yet paid out. Critical for the CRITICAL-002 insolvency fix — before accepting a new pending distribution, the contract checks `yieldPool >= totalPendingDistributions + newAmount`, ensuring it can honor all simultaneous requests.

**Line 31** — `pendingDistributionAmounts[bondId]`. Per-bond pending amount. Used to remove the reservation when `_useYieldPool` is called (at distribution execution time).

**Line 34** — `snapshotAppreciation[bondId]`. The appreciation value captured at `requestDistribution` time (HIGH-002 fix). By snapshotting, any metric manipulation during the 7-day timelock cannot affect the payout amount — the distribution executes at the pre-snapshot value.

**Line 38** — `DEFAULT_MINIMUM_YIELD_POOL = 10 ether`. The initial floor. At 10 ETH, the pool can cover roughly 50 bonds at 0.2 ETH appreciation each before it falls below the minimum.

**Line 39** — `MINIMUM_RESERVE_RATIO = 5000`. In basis points: 5000 / 10000 = 50%. The yield pool should hold at least 50% of the total active bond value. This is a warning threshold — it triggers `ReserveRatioWarning` events but does not in itself revert any transaction (the more conservative `minimumYieldPoolBalance` floor does that).

**Lines 54–57** — Constructor initializes the two critical runtime parameters. Called via super chain when a concrete contract is deployed.

**Lines 87–93** — `fundYieldPool()`. Payable, external, `nonReentrant`. Any address may call it to add ETH to the pool. This supports protocol treasury funding, automated yield strategies, and community contributions. Reverts if `msg.value == 0`.

**Lines 102–116** — `withdrawYieldPool(uint256 amount)`. Owner-only. Checks (a) amount > 0, (b) pool can cover amount, (c) remaining balance after withdrawal >= minimum. Only then sends funds to the owner address. Uses low-level `.call{value: amount}("")` — correct for native ETH transfers as of Solidity 0.8+.

**Lines 123–130** — `setMinimumYieldPoolBalance(uint256 newMinimum)`. Owner-only. Adjusts the floor. The only constraint is `newMinimum > 0`. This allows the owner to tighten or relax the safety floor as protocol TVL grows.

**Lines 139–142** — `setYieldPoolEnforcement(bool enforced)`. Emergency toggle. When set to `false`, the `minimumYieldPoolBalance` floor is bypassed in `_useYieldPool`. The absolute coverage check (`yieldPool >= amount`) still applies — you cannot distribute more than the pool holds.

**Lines 155–184** — `_useYieldPool(uint256 bondId, uint256 amount)`. Internal, called at distribution time. Sequence: (1) require pool >= amount (prevents underflow); (2) if enforced, require pool >= minimum; (3) deduct from pool; (4) remove from `totalPendingDistributions` and clear `pendingDistributionAmounts[bondId]`; (5) emit `YieldPoolUsed`; (6) emit `LowYieldPoolWarning` if pool < 2× minimum. The low-pool warning fires when `yieldPool < 2 * minimumYieldPoolBalance`. The deficit calculation uses a ternary (`yieldPool >= minimum ? 0 : minimum - yieldPool`) to avoid Solidity 0.8 revert on underflow.

**Lines 193–209** — `_trackPendingDistribution(uint256 bondId, int256 appreciationValue)`. Internal, called at `requestDistribution` time. Converts `appreciationValue` to an absolute `uint256` regardless of sign. Checks `yieldPool >= totalPendingDistributions + amount` — the critical solvency check. Snapshots the appreciation value. Updates `pendingDistributionAmounts` and `totalPendingDistributions`.

**Lines 219–222** — `_replenishYieldPool(uint256 bondId, uint256 amount)`. Internal. Called when a bond DEPRECIATES — the loss goes back into the pool to fund future appreciation payouts. This makes the system zero-sum in steady state: losing bonds subsidize gaining bonds.

**Lines 231–234** — `canYieldPoolCover(uint256 amount)`. Public view. Returns two booleans: (a) can the pool cover this amount at all, and (b) would the pool remain above minimum after the deduction. Used by UI/SDK before requesting distribution.

**Lines 241–252** — `getReserveRatio()`. Public view. Returns `(yieldPool * 10000) / totalActiveBondValue` in basis points. If `totalActiveBondValue == 0`, returns 10000 (100%) — there are no bonds to insure. Note: this is a pure calculation with no side effects (the comment about emitting events is a dead note — events cannot be emitted from view functions).

**Lines 262–272** — `getProtocolHealth()`. External view. Combines `yieldPoolOK` (`pool >= minimum`) and `reserveRatioOK` (`ratio >= 5000 bps`) into a single boolean `isHealthy`. Convenience function for dashboards and monitoring.

**Lines 279–287** — `_updateTotalActiveBondValue(uint256 newValue)`. Internal. Replaces `totalActiveBondValue` with `newValue` (child contracts pass `old ± bond.stakeAmount`). After update, checks ratio and emits `ReserveRatioWarning` if below threshold.

## State Variables

| Name | Type | Mutability | Who Can Change | Notes |
|---|---|---|---|---|
| `yieldPool` | `uint256` | Mutable | `fundYieldPool`, `withdrawYieldPool`, `_useYieldPool`, `_replenishYieldPool` | Native-token reserve for payouts |
| `minimumYieldPoolBalance` | `uint256` | Mutable | `setMinimumYieldPoolBalance` (owner) | Pool floor |
| `totalActiveBondValue` | `uint256` | Mutable | `_updateTotalActiveBondValue` (internal — child contracts) | Running sum of active bond stakes |
| `yieldPoolEnforced` | `bool` | Mutable | `setYieldPoolEnforcement` (owner) | Enables/disables floor enforcement |
| `totalPendingDistributions` | `uint256` | Mutable | `_trackPendingDistribution`, `_useYieldPool` | Sum of committed but unpaid distributions |
| `pendingDistributionAmounts` | `mapping(uint256 => uint256)` | Mutable | `_trackPendingDistribution`, `_useYieldPool` | Per-bond pending amount |
| `snapshotAppreciation` | `mapping(uint256 => int256)` | Mutable | `_trackPendingDistribution` (writes), `distributeBond` in children (reads and clears) | Appreciation snapshot to prevent manipulation |
| `DEFAULT_MINIMUM_YIELD_POOL` | `uint256 constant` | Immutable | Nobody | 10 ether |
| `MINIMUM_RESERVE_RATIO` | `uint256 constant` | Immutable | Nobody | 5000 bps (50%) |

## Functions

### `fundYieldPool()` — external payable nonReentrant
Adds `msg.value` to `yieldPool`. Open to any caller. Emits `YieldPoolFunded`. Reverts if `msg.value == 0`.

### `withdrawYieldPool(uint256 amount)` — external onlyOwner nonReentrant
Owner drains excess from pool. Guards: amount > 0, pool can cover, pool would stay above minimum. Sends ETH to `owner`. Emits `YieldPoolWithdrawn`.

### `setMinimumYieldPoolBalance(uint256 newMinimum)` — external onlyOwner
Updates `minimumYieldPoolBalance`. Guard: `newMinimum > 0`. Emits `MinimumYieldPoolUpdated`.

### `setYieldPoolEnforcement(bool enforced)` — external onlyOwner
Toggles `yieldPoolEnforced`. Emits `YieldPoolEnforcementChanged`.

### `_useYieldPool(uint256 bondId, uint256 amount)` — internal
Deducts `amount` from pool. Called by child contract `distributeBond`. Performs all solvency checks and emits appropriate events.

### `_trackPendingDistribution(uint256 bondId, int256 appreciationValue)` — internal
Commits a future payout. Called by child contract `requestDistribution`. Snapshots appreciation. Reverts if pool cannot cover all pending obligations.

### `_replenishYieldPool(uint256 bondId, uint256 amount)` — internal
Adds depreciation back to pool. Called when appreciation is negative.

### `canYieldPoolCover(uint256 amount)` — public view
Returns `(canCover, wouldBeHealthy)`.

### `getReserveRatio()` — public view
Returns ratio in basis points.

### `getProtocolHealth()` — external view
Returns `(isHealthy, yieldPoolOK, reserveRatioOK, currentRatio)`.

### `getYieldPoolBalance()`, `getMinimumYieldPool()`, `isYieldPoolEnforced()` — external view
Simple getters.

## Math: Reserve Ratio

\[
\text{reserveRatio} = \frac{\text{yieldPool} \times 10000}{\text{totalActiveBondValue}}
\]

**Worked example:** If `yieldPool = 15 ether` and `totalActiveBondValue = 20 ether`:

\[
\text{ratio} = \frac{15 \times 10^{18} \times 10000}{20 \times 10^{18}} = 7500 \text{ bps} = 75\%
\]

Since 7500 >= 5000, the protocol is healthy. If `yieldPool` fell to 9 ether:

\[
\text{ratio} = \frac{9 \times 10^{18} \times 10000}{20 \times 10^{18}} = 4500 \text{ bps} = 45\%
\]

This is below `MINIMUM_RESERVE_RATIO` (5000), so `ReserveRatioWarning` fires.

## Events

| Event | When Fired | Parameters |
|---|---|---|
| `YieldPoolFunded` | `fundYieldPool()` | `funder` (indexed), `amount`, `newBalance` |
| `YieldPoolWithdrawn` | `withdrawYieldPool()` | `owner` (indexed), `amount`, `newBalance` |
| `YieldPoolUsed` | `_useYieldPool()` | `bondId` (indexed), `amount`, `remainingBalance` |
| `YieldPoolReplenished` | `_replenishYieldPool()` | `bondId` (indexed), `amount`, `newBalance` |
| `MinimumYieldPoolUpdated` | `setMinimumYieldPoolBalance()` | `oldMinimum`, `newMinimum` |
| `YieldPoolEnforcementChanged` | `setYieldPoolEnforcement()` | `enforced` |
| `LowYieldPoolWarning` | Inside `_useYieldPool()` when pool < 2× min | `currentBalance`, `minimumRequired`, `deficit` |
| `ReserveRatioWarning` | `_updateTotalActiveBondValue()` when ratio < 5000 | `yieldPool`, `activeBonds`, `ratio` |

## Security Invariants

- **Pool can never go below zero** — Solidity 0.8 checked arithmetic + explicit `require(yieldPool >= amount)` guard in `_useYieldPool`.
- **Pending distributions are always reserved** — `_trackPendingDistribution` verifies `yieldPool >= totalPendingDistributions + newAmount` before committing. A second distribution request on a different bond cannot steal the reservation.
- **Owner cannot drain below minimum** — `withdrawYieldPool` checks `yieldPool - amount >= minimumYieldPoolBalance`.
- **Appreciation is snapshotted** — `snapshotAppreciation[bondId]` is set at request time; metrics submitted during the 7-day timelock have no effect on the payout.

## Dependencies

| Import | Notes |
|---|---|
| `./BaseDignityBond.sol` | Provides ReentrancyGuard, Pausable, owner, validation helpers |

## Rebuild Recipe

- **Solc:** `^0.8.25`
- **Optimizer:** 200 runs
- **Constructor args:** none (abstract)
- **Dependencies:** `BaseDignityBond.sol` must be in the same compilation unit or import path

---

# Contract 4: BaseYieldPoolBondV3 (V3.1 pre-audit)

**Filename:** `push_contracts_v3_BaseYieldPoolBondV3.sol`
**Path:** `./push_contracts_v3_BaseYieldPoolBondV3.sol`
**Content hash:** `fda17c1c...`
**Version:** V3.1 — Multi-asset Partner-Ready (pre-V3.3 audit fixes)
**Lines:** 482

## What It Does

`BaseYieldPoolBondV3` extends `BaseDignityBond` (skipping V2's `BaseYieldPoolBond`) and introduces the full V3.1 feature set on top of the V2 yield pool logic:

1. **Multi-asset staking** — bonds can be staked in the chain's native token (ETH on Base, AVAX on Avalanche) or any ERC20 token on the owner-managed whitelist.
2. **Per-bond asset tracking** — `bondStakeToken[bondId]` records which token was used; `bondStakeAmount[bondId]` records how much.
3. **ERC20 yield pools** — each accepted token has its own `tokenYieldPool[token]` balance, separate from the native `yieldPool`.
4. **Timelocked protocol fund withdrawal** — accumulated protocol fees (from forfeited stakes and challenge penalties) can only be withdrawn after a 7-day timelock, fixing audit findings C-01 and C-02 from the 2026 audit round.

This V3.1 file is the pre-audit push candidate. The vf-audit-contracts version (Contract 5) adds CRIT-05 balance-delta accounting.

## Full Source Code

(482 lines — file at `/home/user/workspace/push_contracts_v3_BaseYieldPoolBondV3.sol`)

Reproduced in annotated form below; line numbers match the actual file.

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.25;
     3
     4  import "../BaseDignityBond.sol";
     5  import "./IERC20Minimal.sol";
     6  import "./VaultfireErrors.sol";
     7
     8  // ... NatSpec ...
    26  abstract contract BaseYieldPoolBondV3 is BaseDignityBond {
    27
    28      // ============ Multi-Asset Infrastructure ============
    29
    30      address public constant NATIVE_TOKEN = address(0);
    31      mapping(address => bool) public acceptedTokens;
    32      mapping(address => string) public tokenNames;
    33      address[] public acceptedTokenList;
    34      mapping(uint256 => address) public bondStakeToken;
    35      mapping(uint256 => uint256) public bondStakeAmount;
    36
    37      // ============ Yield Pool State ============
    38
    39      uint256 public yieldPool;
    40      mapping(address => uint256) public tokenYieldPool;
    41      uint256 public minimumYieldPoolBalance;
    42      uint256 public totalActiveBondValue;
    43      bool public yieldPoolEnforced;
    44      uint256 public totalPendingDistributions;
    45      mapping(uint256 => uint256) public pendingDistributionAmounts;
    46      mapping(uint256 => int256) public snapshotAppreciation;
    47
    48      // ============ Locked Funds Withdrawal (V3 FIX: C-01, C-02) ============
    49
    50      uint256 public protocolFunds;
    51
    52      struct WithdrawalRequest {
    53          address destination;
    54          uint256 amount;
    55          uint256 requestedAt;
    56          bool active;
    57      }
    58
    59      WithdrawalRequest public pendingWithdrawal;
    60
    61      uint256 public constant FUND_WITHDRAWAL_TIMELOCK = 7 days;
    62      uint256 public constant DEFAULT_MINIMUM_YIELD_POOL = 10 ether;
    63      uint256 public constant MINIMUM_RESERVE_RATIO = 5000;
    64  }
```

## Line-by-Line Walkthrough

**Line 5** — `import "./IERC20Minimal.sol"`. V3 introduces ERC20 support. The minimal interface exposes only `transferFrom`, `transfer`, `balanceOf`, and `approve`.

**Line 6** — `import "./VaultfireErrors.sol"`. V3 switches from string-literal `require` messages to typed custom errors (4-byte selectors), reducing bytecode size.

**Line 30** — `address public constant NATIVE_TOKEN = address(0)`. Sentinel address. When `bondStakeToken[bondId] == NATIVE_TOKEN` (i.e., `address(0)`), the bond was staked in the native currency. All transfer logic branches on this.

**Lines 31–33** — The token whitelist. `acceptedTokens[token]` is the live gate; `tokenNames[token]` provides UX labels; `acceptedTokenList` is an array for on-chain enumeration. Note: removing a token sets `acceptedTokens[token] = false` but does NOT splice the array — callers should filter by the mapping.

**Lines 34–35** — Per-bond asset tracking. `bondStakeToken[bondId]` = which token (or `address(0)`). `bondStakeAmount[bondId]` = the amount staked.

**Line 40** — `mapping(address => uint256) public tokenYieldPool`. One yield pool per accepted ERC20 token. The native pool is `yieldPool`. This separation ensures that ETH/AVAX yield never gets mixed with USDC yield.

**Lines 50–59** — `protocolFunds` accumulates native-token protocol revenue (forfeited verification stakes, challenge penalties). The `WithdrawalRequest` struct enforces a two-step, timelocked withdrawal. Before V3, there was no timelock and no struct — the owner could drain instantly (findings C-01 and C-02).

**Line 61** — `FUND_WITHDRAWAL_TIMELOCK = 7 days`. Same duration as the distribution timelock. Provides 7 days for governance or community to object to a withdrawal.

**Lines 139–148** — `acceptToken(address token, string calldata name)`. Owner-only. Validates token != address(0), not already accepted, name length 1–50 chars. Pushes to `acceptedTokenList`. Emits `TokenAccepted`.

**Lines 156–159** — `removeToken(address token)`. Owner-only. Sets `acceptedTokens[token] = false`. Existing bonds using this token are unaffected. New bonds cannot be created with the removed token.

**Lines 184–188** — `_recordNativeStake(uint256 bondId, uint256 amount)`. Internal. Sets `bondStakeToken[bondId] = NATIVE_TOKEN` and `bondStakeAmount[bondId] = amount`. Called by child contract `createBond` when `msg.value > 0`.

**Lines 198–209** — `_recordTokenStake(uint256 bondId, address token, uint256 amount, address staker)`. Internal. Validates token is accepted, sets mappings, pulls tokens via `IERC20Minimal.transferFrom`. Reverts with `VF_TokenTransferApproval()` if the transfer returns false (unapproved or insufficient balance). **V3.3 NOTE:** This version does NOT use balance-delta accounting — it trusts the returned `amount` as-is. Fee-on-transfer tokens will silently inflate accounting. The fix lands in Contract 5 (vf-audit-contracts version).

**Lines 215–225** — `_transferStake(uint256 bondId, address payable recipient, uint256 amount)`. Internal. Routes to native call or ERC20 transfer based on `bondStakeToken[bondId]`. For native: low-level `.call{value: amount}("")`. For ERC20: `IERC20Minimal(token).transfer(recipient, amount)`. Both revert with typed errors on failure.

**Lines 232–236** — `fundYieldPool()`. Payable, nonReentrant. Adds `msg.value` to `yieldPool`. Reverts with `VF_MustSendValue()` if zero.

**Lines 243–252** — `fundYieldPoolToken(address token, uint256 amount)`. Non-payable. Accepts ERC20 yield contributions. Pulls tokens via `transferFrom`, adds to `tokenYieldPool[token]`. Emits `YieldPoolFundedToken`.

**Lines 257–268** — `withdrawYieldPool(uint256 amount)`. Owner-only, nonReentrant. Enforces minimum balance constraint. Calls owner via `.call{value: amount}`.

**Lines 284–298** — `requestFundWithdrawal(address destination, uint256 amount)`. Owner-only. Validates address and amount, checks `amount <= protocolFunds`, checks no pending withdrawal is active. Creates `pendingWithdrawal` struct. Emits `FundWithdrawalRequested` with the unlock timestamp.

**Lines 300–315** — `executeFundWithdrawal()`. Owner-only, nonReentrant. Checks: withdrawal active, timelock expired, sufficient funds. Deducts from `protocolFunds`, clears `pendingWithdrawal.active`. Then sends ETH. CEI pattern is partial — the state write (`pendingWithdrawal.active = false`) happens before the call, preventing reentrancy from double-executing.

**Lines 317–321** — `cancelFundWithdrawal()`. Owner-only. Clears `pendingWithdrawal.active`. No funds moved.

**Lines 325–328** — `_accrueProtocolFunds(string memory reason, uint256 amount)`. Internal. Adds to `protocolFunds`. Emits `ProtocolFundsAccrued`. Called by child contracts when stakes are forfeited.

**Lines 334–363** — `_useYieldPool(uint256 bondId, uint256 amount)`. Internal. Routes to native or ERC20 pool based on `bondStakeToken[bondId]`. For native: checks absolute coverage, checks minimum if enforced, deducts, emits `YieldPoolUsed`, optionally emits `LowYieldPoolWarning`. For ERC20: checks coverage in that token's pool, deducts, emits `YieldPoolUsedToken`. In both cases, clears `pendingDistributionAmounts[bondId]` from `totalPendingDistributions`.

**Lines 368–383** — `_trackPendingDistribution(uint256 bondId, int256 appreciationValue)`. Same insolvency-prevention logic as V2, but now routes the pending check to the correct token pool for ERC20 bonds.

**Lines 395–398** — `canYieldPoolCover(uint256 amount)`. View. Returns coverage for the native pool only.

**Lines 400–408** — `getReserveRatio()`. View. Division with `totalActiveBondValue`. **Audit note:** In a mixed-asset deployment, `totalActiveBondValue` includes ETH-denominated and ERC20-denominated bond values — different units. The ratio is only meaningful for native-only deployments. The inline comment explicitly flags this.

## State Variables

| Name | Type | Notes |
|---|---|---|
| `NATIVE_TOKEN` | `address constant` | address(0) sentinel |
| `acceptedTokens` | `mapping(address => bool)` | Whitelist gate |
| `tokenNames` | `mapping(address => string)` | UX labels |
| `acceptedTokenList` | `address[]` | Enumerable list (may contain removed tokens) |
| `bondStakeToken` | `mapping(uint256 => address)` | Per-bond token type |
| `bondStakeAmount` | `mapping(uint256 => uint256)` | Per-bond staked amount |
| `yieldPool` | `uint256` | Native yield reserve |
| `tokenYieldPool` | `mapping(address => uint256)` | Per-token yield reserves |
| `minimumYieldPoolBalance` | `uint256` | Native pool floor |
| `totalActiveBondValue` | `uint256` | Sum of active bond stakes |
| `yieldPoolEnforced` | `bool` | Floor enforcement flag |
| `totalPendingDistributions` | `uint256` | Committed but unpaid (native) |
| `pendingDistributionAmounts` | `mapping(uint256 => uint256)` | Per-bond pending |
| `snapshotAppreciation` | `mapping(uint256 => int256)` | Manipulation-proof snapshot |
| `protocolFunds` | `uint256` | Accumulated native protocol revenue |
| `pendingWithdrawal` | `WithdrawalRequest` | Active withdrawal request |
| `FUND_WITHDRAWAL_TIMELOCK` | `uint256 constant` | 7 days = 604800 s |
| `DEFAULT_MINIMUM_YIELD_POOL` | `uint256 constant` | 10 ether |
| `MINIMUM_RESERVE_RATIO` | `uint256 constant` | 5000 bps |

## Events

| Event | When | Parameters |
|---|---|---|
| `YieldPoolFunded` | `fundYieldPool()` | `funder`, `amount`, `newBalance` |
| `YieldPoolFundedToken` | `fundYieldPoolToken()` | `funder`, `token`, `amount`, `newBalance` |
| `YieldPoolWithdrawn` | `withdrawYieldPool()` | `owner`, `amount`, `newBalance` |
| `YieldPoolUsed` | `_useYieldPool()` (native) | `bondId`, `amount`, `remainingBalance` |
| `YieldPoolUsedToken` | `_useYieldPool()` (ERC20) | `bondId`, `token`, `amount`, `remainingBalance` |
| `YieldPoolReplenished` | `_replenishYieldPool()` (native) | `bondId`, `amount`, `newBalance` |
| `MinimumYieldPoolUpdated` | `setMinimumYieldPoolBalance()` | `oldMinimum`, `newMinimum` |
| `YieldPoolEnforcementChanged` | `setYieldPoolEnforcement()` | `enforced` |
| `LowYieldPoolWarning` | `_useYieldPool()` when pool < 2×min | `currentBalance`, `minimumRequired`, `deficit` |
| `ReserveRatioWarning` | `_updateTotalActiveBondValue()` | `yieldPool`, `activeBonds`, `ratio` |
| `ProtocolFundsAccrued` | `_accrueProtocolFunds()` | `reason`, `amount`, `newTotal` |
| `FundWithdrawalRequested` | `requestFundWithdrawal()` | `destination` (indexed), `amount`, `availableAt` |
| `FundWithdrawalExecuted` | `executeFundWithdrawal()` | `destination` (indexed), `amount` |
| `FundWithdrawalCancelled` | `cancelFundWithdrawal()` | (none) |
| `TokenAccepted` | `acceptToken()` | `token` (indexed), `name` |
| `TokenRemoved` | `removeToken()` | `token` (indexed) |
| `BondStakedNative` | `_recordNativeStake()` | `bondId` (indexed), `amount` |
| `BondStakedToken` | `_recordTokenStake()` | `bondId` (indexed), `token` (indexed), `amount` |

## Security Invariants

- **A bond's staked token is immutable** — `bondStakeToken[bondId]` is written once in `_recordNativeStake` / `_recordTokenStake` and never updated.
- **Yield pools are segregated** — native `yieldPool` and per-token `tokenYieldPool` cannot be used interchangeably. An ERC20 bond never draws from the native pool.
- **Protocol funds require 7-day timelock** — only one pending withdrawal can exist at a time; owner must wait 7 days.
- **Only one pending withdrawal active at once** — `requestFundWithdrawal` reverts if `pendingWithdrawal.active` is already true.

## Dependencies

| Import | Purpose |
|---|---|
| `../BaseDignityBond.sol` | Owner, pause, reentrancy, validation |
| `./IERC20Minimal.sol` | ERC20 operations |
| `./VaultfireErrors.sol` | Typed custom errors |

## Rebuild Recipe

- **Solc:** `^0.8.25`
- **Optimizer:** 200 runs
- **Constructor args:** none (abstract)
- **Import path differences:** note `../BaseDignityBond.sol` vs `./BaseDignityBond.sol` — depends on which directory this is deployed from

---

# Contract 5: BaseYieldPoolBondV3 (V3.3 post-audit)

**Filename:** `BaseYieldPoolBondV3.sol`
**Path:** `./vf-audit-contracts/BaseYieldPoolBondV3.sol`
**Content hash:** `9c5730d0...`
**Version:** V3.3 — adds CRIT-05 (balance-delta), C-01 (`remainingStake`), H-07 (`totalPendingTokenDistributions`), HIGH-04 (`protocolFundsToken`)
**Lines:** 535

## What It Does

This is the **audited production version** of `BaseYieldPoolBondV3`. It incorporates four critical changes on top of V3.1:

1. **CRIT-05 (balance-delta for ERC20 transfers):** `_recordTokenStake` now measures the actual contract balance before and after `transferFrom`, using the delta as the true received amount. This prevents fee-on-transfer tokens from inflating accounting.
2. **C-01 (`remainingStake` tracking):** A new `mapping(uint256 => uint256) public remainingStake` is introduced. It starts at the staked amount and is decremented by `_transferStake`. `deactivateBond` uses `remainingStake` instead of `bond.stakeAmount` to return only what is actually available — preventing over-withdrawal after distributions.
3. **H-07 (`totalPendingTokenDistributions`):** A per-token analog to `totalPendingDistributions` so ERC20 bonds can also prevent double-commitment of the same token pool.
4. **HIGH-04 (`protocolFundsToken`):** A per-token mapping tracks ERC20 protocol fund accumulation separately from native `protocolFunds`, so challenge penalties for ERC20 bonds can be paid from the correct token.

## Key Differences from V3.1 (diff summary)

```diff
+ mapping(uint256 => uint256) public remainingStake;
+ mapping(address => uint256) public totalPendingTokenDistributions;
+ mapping(address => uint256) public protocolFundsToken;
+ event ProtocolFundsTokenAccrued(address indexed token, string reason, uint256 amount, uint256 newTotal);

  _recordNativeStake():
+     remainingStake[bondId] = amount;   // C-01 FIX

  _recordTokenStake():
+     // Balance-delta pattern — measure actual received tokens
+     uint256 balBefore = IERC20Minimal(token).balanceOf(address(this));
      bool success = IERC20Minimal(token).transferFrom(staker, address(this), amount);
+     uint256 actualReceived = IERC20Minimal(token).balanceOf(address(this)) - balBefore;
+     bondStakeToken[bondId] = token;
+     bondStakeAmount[bondId] = actualReceived;
+     remainingStake[bondId] = actualReceived;
+     emit BondStakedToken(bondId, token, actualReceived);

  _transferStake():
+     if (remainingStake[bondId] < amount) revert VF_InsufficientBondStake();
+     remainingStake[bondId] -= amount;   // decrement remaining
```

## Line-by-Line Walkthrough (new/changed lines only)

**`remainingStake` mapping (new):** Tracks how much of the original stake is still available for return or further distribution. Initially equals `bondStakeAmount`. After each distribution cycle pays out from the yield pool and credits `remainingStake` (in child `distributeBond`), the counter reflects the bond's current ETH backing. When `deactivateBond` is called, this value — not `bond.stakeAmount` — is returned to the owner.

**Balance-delta in `_recordTokenStake`:** Before `transferFrom`, reads `balBefore = IERC20Minimal(token).balanceOf(address(this))`. After the call, reads `balAfter` and computes `actualReceived = balAfter - balBefore`. All downstream state (`bondStakeAmount`, `remainingStake`, and the emitted event) use `actualReceived` not the nominal `amount`. This correctly handles tokens with transfer fees (e.g., USDC in certain jurisdictions, PAXG, etc.).

**`VF_InsufficientBondStake` error in `_transferStake`:** An explicit check ensures the transfer amount does not exceed `remainingStake[bondId]`. This catches both programming errors and any reentrancy attempt that might reduce remaining stake between calls.

**`totalPendingTokenDistributions[token]`:** Used in `_trackPendingDistribution` for ERC20 bonds, analogously to `totalPendingDistributions` for native bonds. Prevents two pending ERC20 bond requests from both claiming the same token pool reserves.

**`protocolFundsToken[token]`:** Accumulated ERC20 protocol revenue. `_accrueProtocolFunds` has an ERC20 variant that updates this mapping. The `resolveChallenge` function in `AIAccountabilityBondsV3` draws from this when paying penalties for challenged ERC20 bonds.

## Security Invariants Added by V3.3

- **Fee-on-transfer tokens cannot inflate accounting** — balance-delta means `bondStakeAmount[bondId] <= msg.value` (or `stakeAmount` parameter) always, never greater.
- **Over-withdrawal after distributions impossible** — `remainingStake` is decremented on every outgoing transfer; `deactivateBond` returns exactly `remainingStake[bondId]`.
- **ERC20 protocol funds are segregated** — `protocolFundsToken[token]` ensures ERC20 challenge penalties come from ERC20 revenue, not native ETH.

## Rebuild Recipe

- **Solc:** `^0.8.25`
- **Optimizer:** 200 runs
- **File:** `./vf-audit-contracts/BaseYieldPoolBondV3.sol` with `../BaseDignityBond.sol`, `./IERC20Minimal.sol`, `./VaultfireErrors.sol`
- **This is the audited deployment target**

---

# Contract 6: BaseYieldPoolBondV3 (v3 and v3-pr-ready variants)

**Paths:**
- `./v3/BaseYieldPoolBondV3.sol` (hash `13d7626...`)
- `./v3-pr-ready/contracts/v3/BaseYieldPoolBondV3.sol` (hash `cb05c081...`, identical to v3-pr-ready; 491 lines)

**Version:** V3.1 — intermediate snapshots before the full V3.3 audit fixes

Both files are intermediate drafts between the push-contracts V3.1 and the vf-audit-contracts V3.3 final. The `v3-pr-ready` version switches from typed custom errors (`VF_*`) back to `require` strings (a reversion for compatibility testing), and import paths use `./BaseDignityBond.sol` rather than `../BaseDignityBond.sol`.

The logical content is identical to Contract 4 except for these syntactic differences. They are documented here by reference; for full line-by-line analysis, see Contracts 4 and 5.

**Key difference from push-contracts (Contract 4):** v3-pr-ready uses `require(condition, "string")` in place of `revert VF_*()` throughout. All logic is equivalent. This variant exists for chains or tooling that display require-string errors more readably than typed-error selectors.

---

# Contract 7: AIAccountabilityBondsV2

**Filename:** `AIAccountabilityBondsV2.sol`
**Path:** `./AIAccountabilityBondsV2.sol`
**Content hash:** `45b1970a...`
**Version:** V2 — production-ready, audit-remediated
**Lines:** 1139

## What It Does

`AIAccountabilityBondsV2` is the first full production release of the AI Accountability Bond mechanism. It implements an economic contract between an AI company and humanity: the AI company stakes 30% of its quarterly revenue; profits are distributed proportionally to global human flourishing scores. If the global flourishing score falls below 4000 (40%), profits are 100% locked and sent to the human treasury. The bond appreciates or depreciates based on changes in six global metrics tracked on-chain.

This contract includes the full V2 audit remediation: oracle peer-verification, metrics challenges, a human verification system, and the solvency-protecting yield pool inherited from `BaseYieldPoolBond`.

## Full Source Code

(1139 lines — canonical file at `/home/user/workspace/AIAccountabilityBondsV2.sol`)

Key sections are excerpted and annotated throughout this documentation section.

## Line-by-Line Walkthrough

### Imports and Contract Declaration (lines 1–24)

```solidity
import "./BaseYieldPoolBond.sol";
import "./MissionEnforcement.sol";
```

Two imports. `BaseYieldPoolBond` provides the yield pool machinery. `MissionEnforcement` is an optional policy contract (disabled by default) that checks three core principles: AI profit caps, community challenges, and privacy defaults.

```solidity
contract AIAccountabilityBondsV2 is BaseYieldPoolBond {
```

Concrete (non-abstract) contract. The full inheritance chain is: `AIAccountabilityBondsV2` → `BaseYieldPoolBond` → `BaseDignityBond` → `ReentrancyGuard` + `Pausable`.

### Structs

**`Bond`** — Core bond record. Fields:
- `bondId`: Sequential uint256 ID starting from 1.
- `aiCompany`: The creating company address; only this address can call `onlyAICompany` functions.
- `companyName`: String, 1–100 chars.
- `quarterlyRevenue`: The claimed revenue (not verified on-chain; used to enforce the 30% minimum stake).
- `stakeAmount`: `msg.value` at creation time.
- `createdAt`, `distributionRequestedAt`: Timestamps for lifecycle tracking.
- `distributionPending`: Set to true during the 7-day timelock; prevents creating another pending distribution.
- `active`: Bond exists and can receive metrics/verifications.

**`GlobalFlourishingMetrics`** — Six scores submitted by the AI company. All on the 0–10000 scale:
- `incomeDistributionScore` — Is wealth being distributed or concentrated?
- `povertyRateScore` — Are people escaping poverty?
- `healthOutcomesScore` — Is life expectancy improving?
- `mentalHealthScore` — Are depression/anxiety rates falling?
- `educationAccessScore` — Can people learn AI-relevant skills?
- `purposeAgencyScore` — Do people have meaningful activities (paid OR unpaid)? This is the key "works with zero employment" metric.

**`OracleSource`** — Registered data source. Has `trustScore` threshold enforcement: oracles below 7000 (70%) are rejected.

**`AIVerification`** — A peer-company verification. The verifying AI stakes a small amount; if their verification proves correct (the challenged bond was honestly reported), they get the stake back via `settleVerificationStake`. If they were wrong, the stake is forfeited to `protocolFunds`.

**`MetricsChallenge`** — Community members can stake 0.1 ETH to challenge suspicious metrics. If upheld by the owner, the challenger receives 2× their stake (their original plus a penalty from `protocolFunds`). If denied, the stake accrues to `protocolFunds`.

### State Variables (lines ~125–175)

- `nextBondId = 1` — Bond IDs start at 1, not 0. `0` is used as a sentinel "not found" value.
- `lastDistributedValue[bondId]` — The bond value recorded at last distribution. Appreciation is `currentValue - lastDistributedValue`. This prevents a bond from paying out the same appreciation twice across two distribution cycles.
- `humanTreasury` — `address payable` that receives the human share. Set in constructor; owner-updatable.
- `oracles` / `registeredOracles` — Oracle registry. Only owner can register.
- `verificationConfirmCount[bondId]` / `verificationRejectCount[bondId]` — O(1) counters, replacing an O(n) array scan in earlier versions.
- `activeChallengeCount[bondId]` — Number of unresolved challenges. Distributions can be blocked pending challenges (implementation depends on child contract logic).
- `MIN_CHALLENGE_STAKE = 0.1 ether` — Anti-spam floor for challenges.
- `SUFFERING_THRESHOLD = 4000` — Below this score, profits are 100% redirected to humans.
- `LOW_INCLUSION_THRESHOLD = 4000` — Education + purpose combined threshold for inclusion check.
- `MIN_AI_VERIFICATIONS = 2` — Minimum peer verifications required.
- `MIN_ORACLE_TRUST_SCORE = 7000` — 70% trust score minimum.

### Constructor (line ~290)

```solidity
constructor(address payable _humanTreasury) {
    require(_humanTreasury != address(0), "Human treasury cannot be zero address");
    humanTreasury = _humanTreasury;
    missionEnforcementEnabled = false;
}
```

Takes one parameter: the human treasury address. Mission enforcement is disabled by default — the protocol must be explicitly activated via `setMissionEnforcementEnabled(true)`.

### `createBond(string memory companyName, uint256 quarterlyRevenue)` (lines ~350–390)

- Visibility: `external payable`
- Modifiers: `whenNotPaused`
- Checks:
  - `msg.value > 0`
  - `msg.value >= (quarterlyRevenue * 30) / 100` — must stake at least 30% of revenue
  - `companyName` length 1–100
- Creates `Bond` struct, assigns ID, stores `lastDistributedValue[bondId] = msg.value`.
- Calls `_updateTotalActiveBondValue(totalActiveBondValue + msg.value)` — updates reserve ratio tracking.
- Returns `bondId`.

### `submitMetrics(...)` (lines ~400–440)

Seven parameters: `bondId` + 6 flourishing scores. Only the AI company can call this. Validates all scores <= 10000. Creates a `GlobalFlourishingMetrics` entry in `bondMetrics[bondId]`. Emits `MetricsSubmitted` with the computed `globalFlourishingScore`. **V3 note:** In V2, calling `submitMetrics` does NOT reset oracle confirmation state — that reset is a V3 addition.

### `globalFlourishingScore(uint256 bondId)` (view)

Returns the simple arithmetic average of the 6 latest metrics:

\[
\text{score} = \frac{\text{income} + \text{poverty} + \text{health} + \text{mental} + \text{education} + \text{purpose}}{6}
\]

If no metrics submitted, returns 5000 (neutral midpoint).

### `calculateBondValue(uint256 bondId)` (view)

```solidity
return bonds[bondId].stakeAmount * globalFlourishingScore(bondId) / 10000;
```

Linear scaling: if flourishing score = 10000, bond value = stake. If score = 5000, bond value = 50% of stake. If score = 0, bond value = 0.

### `calculateAppreciation(uint256 bondId)` (view)

```solidity
return int256(calculateBondValue(bondId)) - int256(lastDistributedValue[bondId]);
```

Signed integer. Positive = appreciation (AI profits). Negative = depreciation (AI supports humans from pool).

### `requestDistribution(uint256 bondId)` (onlyAICompany, bondExists, whenNotPaused)

- Calls `_requireMissionCompliance()` (no-op unless enabled).
- Reverts if `distributionPending` already.
- In V2, large bonds do NOT require oracle confirmation here (that is V3 addition).
- Calls `_trackPendingDistribution(bondId, appreciation)` — snapshots appreciation, reserves pool funds.
- Sets `distributionPending = true`, records `distributionRequestedAt`.

### `distributeBond(uint256 bondId)` (nonReentrant, whenNotPaused, onlyAICompany, bondExists)

Core payout function. After 7-day timelock:

1. Reads `snapshotAppreciation[bondId]`.
2. Checks `shouldLockProfits(bondId)` — if score < 4000, `locked = true`.
3. **If appreciation positive:**
   - Calls `_useYieldPool(bondId, absAmount)` — deducts from pool.
   - If locked: `humanShare = absAmount`, `aiShare = 0`.
   - If not locked: `humanShare = absAmount / 2` (50–50 split). Note: V2 uses 50/50 (integer rounding gives human the odd wei).
4. **If appreciation negative (depreciation):**
   - `_useYieldPool(bondId, absDepreciation)` — draws from pool to compensate.
   - `humanShare = absDepreciation`, `aiShare = 0`.
5. Pushes `Distribution` record.
6. Bond stays ACTIVE (this is the "multi-cycle" fix — bonds are not deactivated on distribution).
7. Transfers: `aiShare` to `bond.aiCompany`, `humanShare` to `humanTreasury`.

**Distribution math worked example:**

Suppose:
- `stakeAmount = 10 ether`
- Latest global flourishing score = 6000 (60%)
- `lastDistributedValue = 10 ether` (initial)
- `calculateBondValue = 10 ether * 6000 / 10000 = 6 ether`
- `appreciation = 6 ether - 10 ether = -4 ether` (depreciation)
- Since appreciation is negative: `_useYieldPool(bondId, 4 ether)` draws 4 ETH from pool.
- `humanShare = 4 ether`, paid to `humanTreasury`.

Now if flourishing improves to 8000 next cycle:
- `calculateBondValue = 10 ether * 8000 / 10000 = 8 ether`
- `lastDistributedValue` was updated to 6 ether (the value at last distribution).
- `appreciation = 8 ether - 6 ether = 2 ether` (appreciation).
- `_useYieldPool(bondId, 2 ether)`.
- If not locked: `humanShare = 1 ether`, `aiShare = 1 ether`.

### `shouldLockProfits(uint256 bondId)` (public view)

Returns `(locked bool, reason string)`. Locked if:
- `globalFlourishingScore < SUFFERING_THRESHOLD (4000)`, OR
- `educationAccessScore + purposeAgencyScore < LOW_INCLUSION_THRESHOLD (4000)`

These are the "AI has fired everyone" checks. If humans are suffering regardless of employment, AI captures nothing.

### Oracle and Verification System

`registerOracle()` — Owner adds trusted data sources with trust scores.
`submitAIVerification()` — Competing AI companies stake 0.01+ ETH to confirm or challenge another company's metrics.
`settleVerificationStake()` — After distribution, verifier retrieves stake if they confirmed correctly. If they rejected and the bond distributed (proving metrics were correct), stake goes to protocol.
`challengeMetrics()` — Anyone stakes 0.1+ ETH to formally challenge.
`resolveChallenge()` — Owner decides. If upheld: challenger gets 2× stake (stakeAmount + penalty from protocolFunds). If denied: challenger's stake goes to protocolFunds.

### `deactivateBond(uint256 bondId)` (onlyAICompany, bondExists)

Sets `bond.active = false`. Updates `totalActiveBondValue`. Returns `bond.stakeAmount` to `bond.aiCompany`. **V2 BUG note:** Returns the original `stakeAmount`, not accounting for distributions that may have already paid portions out. V3 fixes this with `remainingStake`.

## Math: Bond Value Formula

For V2:

\[
\text{bondValue}(t) = \text{stakeAmount} \times \frac{\text{globalFlourishingScore}(t)}{10000}
\]

\[
\text{appreciation}(t) = \text{bondValue}(t) - \text{lastDistributedValue}
\]

**Distribution splits (V2):**

| Condition | Human Share | AI Share |
|---|---|---|
| Score < 4000 (suffering) | 100% | 0% |
| Score >= 4000 AND appreciation > 0 | 50% | 50% |
| Appreciation < 0 (depreciation) | 100% compensation | 0% |

## Events

| Event | When | Key Parameters |
|---|---|---|
| `BondCreated` | `createBond()` | `bondId`, `aiCompany`, `companyName`, `quarterlyRevenue`, `stakeAmount`, `timestamp` |
| `HumanTreasuryUpdated` | constructor / `setHumanTreasury()` | `previousTreasury`, `newTreasury` |
| `MetricsSubmitted` | `submitMetrics()` | `bondId`, `timestamp`, `globalFlourishingScore` |
| `DistributionRequested` | `requestDistribution()` | `bondId`, `aiCompany`, `requestedAt`, `availableAt` |
| `BondDistributed` | `distributeBond()` | `bondId`, `aiCompany`, `humanShare`, `aiCompanyShare`, `appreciation`, `reason`, `timestamp` |
| `ProfitsLocked` | `distributeBond()` when suffering | `bondId`, `reason`, `timestamp` |
| `OracleRegistered` | `registerOracle()` | `oracleAddress`, `sourceName`, `trustScore`, `timestamp` |
| `AIVerificationSubmitted` | `submitAIVerification()` | `bondId`, `verifyingAI`, `confirmsMetrics`, `stakeAmount`, `timestamp` |
| `MetricsChallenged` | `challengeMetrics()` | `bondId`, `challenger`, `reason`, `challengeStake`, `timestamp` |
| `ChallengeResolved` | `resolveChallenge()` | `bondId`, `challengeIndex`, `challengeUpheld`, `timestamp` |
| `MissionEnforcementUpdated` | `setMissionEnforcement()` | `previous`, `current` |
| `MissionEnforcementEnabled` | `setMissionEnforcementEnabled()` | `enabled` |

## Security Invariants

- **30% stake enforcement** — `msg.value >= (quarterlyRevenue * 30) / 100`; an AI company cannot create a bond with less than 30% of its claimed revenue.
- **7-day timelock** — between `requestDistribution` and `distributeBond`.
- **Solvency** — `_trackPendingDistribution` ensures pool can cover all pending obligations before committing.
- **Single pending distribution** — `distributionPending` prevents requesting a second while the first is active.
- **Score range** — all submitted scores are bounded [0, 10000].
- **Human treasury cannot be zero** — constructor and `setHumanTreasury` both check this.

## Dependencies

| Contract | Role |
|---|---|
| `BaseYieldPoolBond` | Yield pool, nonReentrant, Pausable, owner |
| `MissionEnforcement` | Optional policy gate (disabled by default) |

## Rebuild Recipe

- **Solc:** `^0.8.25`, optimizer 200 runs
- **Constructor arg:** `address payable _humanTreasury` — address that receives human share of distributions
- **Deployment order:** 1. Deploy `BaseDignityBond` (no — abstract), 2. Deploy `BaseYieldPoolBond` (no — abstract), 3. Deploy `AIAccountabilityBondsV2(_humanTreasury)`
- **Post-deploy:** Call `fundYieldPool()` to fund distribution reserve. Optionally register oracles. Optionally enable mission enforcement.

---

# Contract 8: AIAccountabilityBondsV2 (theloopbreaker-site variant)

**Path:** `./theloopbreaker-site/contracts/AIAccountabilityBondsV2.sol`
**Content hash:** `c0919c05...` (distinct from root workspace)
**Version:** V2 — earlier snapshot from theloopbreaker-site branch

This is a slightly older version of `AIAccountabilityBondsV2`. The logic is substantially identical; minor differences include the import path for `MissionEnforcement` and some string messages. The `lastDistributedValue` mechanism and the full audit remediation are present. For complete documentation, see Contract 7. The key delta: this version uses `require(...)` with string messages rather than the typed-error approach that appears in later pushes.

---

# Contract 9: AIAccountabilityBondsV3 (V3.3 final — root workspace / vf-audit-contracts)

**Filename:** `AIAccountabilityBondsV3.sol`
**Paths:**
- `./AIAccountabilityBondsV3.sol` (hash `f719ffe6...`)
- `./vf-audit-contracts/AIAccountabilityBondsV3.sol` (same hash — identical files)
**Version:** V3.3 — audited production
**Lines:** 1111

## What It Does

`AIAccountabilityBondsV3` is the production-deployed version of the AI Accountability Bond system. It extends `BaseYieldPoolBondV3` (multi-asset), replacing V2's `BaseYieldPoolBond`. It adds:

1. **Multi-asset staking** — `createBond()` for native token, `createBondWithToken()` for ERC20.
2. **Cycle-specific oracle quorum confirmation** — metrics are confirmed by a quorum of oracles; the confirmation is tied to a specific metrics submission index to prevent stale confirmations.
3. **Oracle TWAP cross-checking** — when `oracleEnforced = true`, self-reported scores are capped to ±20% of the on-chain oracle TWAP via `IFlourishingMetricsOracle`.
4. **Fee-on-transfer safety** — `createBondWithToken` reads `remainingStake[bondId]` post-`_recordTokenStake` to get the actual staked amount.
5. **Bridge deactivation notification** — `deactivateBond` calls `bridgeNotifier.notifyAccountabilityBondDeactivated(bondId)` so remote chains can mark the bond inactive.
6. **ERC20 challenge penalties** — `resolveChallenge` draws from `protocolFundsToken[token]` for ERC20 bonds.

## Detailed State Variables

Beyond the V2 set, V3 adds:

| Variable | Type | Purpose |
|---|---|---|
| `oracleRequiredThreshold` | `uint256` | Stake threshold above which oracle confirmation is mandatory (default 10 ETH). |
| `oracleConfirmedMetrics[bondId]` | `mapping(uint256 => bool)` | Whether the current metrics have reached quorum confirmation. Reset on every `submitMetrics` call. |
| `oracleConfirmationCount[bondId]` | `mapping(uint256 => uint256)` | Count of distinct oracle confirmations this cycle. |
| `oracleConfirmedAtCycle[bondId][oracle]` | `mapping(uint256 => mapping(address => uint256))` | Cycle counter at which each oracle last confirmed. Prevents double-counting within a cycle. |
| `bondConfirmCycle[bondId]` | `mapping(uint256 => uint256)` | Incremented on every `submitMetrics`. Makes old oracle confirmations stale. |
| `oracleQuorum` | `uint256` | Minimum distinct oracle confirmations required (default 2). |
| `flourishingOracle` | `IFlourishingMetricsOracle` | External oracle contract for TWAP data. |
| `oracleEnforced` | `bool` | When true, oracle TWAP caps self-reported scores. |
| `ORACLE_TOLERANCE_BPS` | `uint256 constant` | 2000 = 20% max deviation from oracle. |
| `confirmedMetricsIndex[bondId]` | `mapping(uint256 => uint256)` | Which `bondMetrics` array index was confirmed by the quorum. |
| `confirmedAtQuorum[bondId]` | `mapping(uint256 => uint256)` | The `oracleQuorum` value in effect when the bond reached confirmation. Guards against quorum-raising bypasses. |
| `bridgeNotifier` | `IVaultfireBridgeNotifier` | Optional cross-chain bridge notification contract. |
| `MIN_VERIFICATION_STAKE` | `uint256 constant` | 0.01 ether — H-02 fix: prevents dust stakes. |
| `METRIC_*` | `bytes32 constant` | keccak256 hashes of metric names for oracle lookup keys. |

## Function Deep-Dives

### `setOracleQuorum(uint256 newQuorum)` — onlyOwner

Sets the minimum number of distinct, qualified oracles required to confirm a bond's metrics. Emits `OracleQuorumUpdated`. V3.3-r2 fix: when the quorum is raised, bonds confirmed at the lower quorum are invalidated at `requestDistribution` time via `confirmedAtQuorum[bondId] < oracleQuorum` check.

### `oracleConfirmMetrics(uint256 bondId)` — external

Called by registered oracle addresses. Steps:
1. Check `oracles[msg.sender].isActive` and `trustScore >= 7000`.
2. Check `bondMetrics[bondId].length > 0`.
3. **CRIT-08 / NEW-03 fix:** Check `oracleConfirmedAtCycle[bondId][msg.sender] != bondConfirmCycle[bondId]`. If the oracle has already confirmed in this cycle, revert `AB_OracleAlreadyConfirmed()`. Then write `oracleConfirmedAtCycle[bondId][msg.sender] = bondConfirmCycle[bondId]`.
4. Increment `oracleConfirmationCount[bondId]`.
5. If `count >= oracleQuorum`: set `oracleConfirmedMetrics[bondId] = true`, record `confirmedMetricsIndex[bondId]` and `confirmedAtQuorum[bondId] = oracleQuorum`.
6. Emit `OracleMetricsConfirmed`.

### `submitMetrics(...)` — onlyAICompany(bondId), bondExists, whenNotPaused

Same six scores as V2. Key V3 additions:
- Resets `oracleConfirmedMetrics[bondId] = false`.
- **NEW-03 fix:** Increments `bondConfirmCycle[bondId]++` — O(1) oracle confirmation reset. (V2 and early V3 had an O(n) loop over `registeredOracles` to reset per-oracle booleans; V3.3-r2 replaces this with the cycle counter pattern.)
- Resets `oracleConfirmationCount[bondId] = 0`.

### `requestDistribution(uint256 bondId)` — onlyAICompany, bondExists, whenNotPaused

Additional V3 checks before the V2 flow:
1. `bond.stakeAmount >= oracleRequiredThreshold` → requires `oracleConfirmedMetrics[bondId] == true`.
2. **HIGH-03 fix:** `confirmedMetricsIndex[bondId] != bondMetrics[bondId].length - 1` → revert `AB_ConfirmationStale()`. The confirmation must be for the most recently submitted metrics.
3. **NEW-01-r2 fix:** `confirmedAtQuorum[bondId] < oracleQuorum` → revert `AB_ConfirmationStale()`. If quorum was raised after this bond was confirmed, must re-confirm.

### `distributeBond(uint256 bondId)` — nonReentrant, whenNotPaused, onlyAICompany, bondExists

V3.3 CEI order (from inline comments):

```
1. bond.distributionPending = false          (effect — clear re-entrancy path)
2. int256 appreciation = snapshotAppreciation[bondId]  (read)
3. lastDistributedValue[bondId] = currentValue         (effect)
4. shouldLockProfits check                             (read)
5. _useYieldPool(bondId, absAmount)                    (effect — internal accounting)
6. Calculate humanShare, aiCompanyShare                (compute)
7. Push to bondDistributions[]                         (effect)
8. bond.distributionCount++                            (effect)
9. snapshotAppreciation[bondId] = 0                    (effect)
10. remainingStake[bondId] += absAmount                (effect — CRITICAL: MUST precede transfers)
11. _transferStake to aiCompany                        (interaction)
12. _transferStake to humanTreasury                    (interaction)
```

Step 10 must precede step 11/12 because `_transferStake` checks `remainingStake[bondId] >= amount` before decrementing. The credit-then-debit is atomic within one `nonReentrant` transaction: if any `_transferStake` reverts, the entire transaction reverts, rolling back step 10 as well. This eliminates the "phantom balance" vulnerability of CRIT-04.

**Distribution splits (V3.3):**

| Condition | Human Share | AI Share |
|---|---|---|
| Appreciation > 0, profits locked (score < 4000) | 100% | 0% |
| Appreciation > 0, not locked | 50% (rounding: `(abs+1)/2`) | 50% |
| Appreciation < 0 (depreciation) | 100% compensation | 0% |

Note: V3 uses `(absAmount + 1) / 2` for `humanShare` when not locked, ensuring the human always gets the larger half on odd-wei amounts.

### `globalFlourishingScore(uint256 bondId)` — public view

V3.3 CRIT-03 addition: when `oracleEnforced && address(flourishingOracle) != address(0)`, each score is passed through `_capToOracle(score, metricId)` before averaging.

`_capToOracle(uint256 selfReported, bytes32 metricId)`:

```solidity
function _capToOracle(uint256 selfReported, bytes32 metricId) internal view returns (uint256) {
    if (!flourishingOracle.isMetricFresh(metricId)) return selfReported;
    uint256 twap = flourishingOracle.getMetricTWAP(metricId, 0);
    uint256 upperBound = twap + (twap * ORACLE_TOLERANCE_BPS) / 10000;
    if (selfReported > upperBound) return upperBound;
    return selfReported;
}
```

**Worked example:** If oracle TWAP for `incomeDistribution = 5000`, tolerance = 20% → `upperBound = 5000 + 1000 = 6000`. If AI company submits 8000, it is capped to 6000 for the score average. This prevents a single metric from being inflated to maximise appreciation.

### `resolveChallenge(uint256 bondId, uint256 challengeIndex, bool upheld)` — onlyOwner, nonReentrant

HIGH-04 fix: checks `bondStakeToken[bondId]` to route penalty payment:
- If `NATIVE_TOKEN`: deducts from `protocolFunds`, pays challenger `stakeAmount + penalty` in ETH.
- If ERC20 token: deducts from `protocolFundsToken[token]`. Pays native challenge stake back to challenger in ETH (challenge stakes are always native). Also pays ERC20 penalty from `protocolFundsToken[token]`.

### `deactivateBond(uint256 bondId)` — onlyAICompany, bondExists, nonReentrant

CRIT-06 fix: uses `remainingStake[bondId]` (not `bond.stakeAmount`) for `totalActiveBondValue` decrement. Returns `remainingStake[bondId]` to `bond.aiCompany`.

HIGH-18 fix: optionally notifies `bridgeNotifier`:
```solidity
if (address(bridgeNotifier) != address(0)) {
    try bridgeNotifier.notifyAccountabilityBondDeactivated(bondId) {
        emit BondDeactivationNotified(bondId);
    } catch {} // best-effort
}
```
The `try/catch` with empty catch ensures bridge failure never blocks deactivation.

## Math: Oracle Cross-Check

When `oracleEnforced = true`:

\[
\text{cappedScore} = \min\!\left(\text{selfReported},\ \text{oracleTWAP} \times \frac{10000 + 2000}{10000}\right) = \min\!\left(\text{selfReported},\ \text{oracleTWAP} \times 1.2\right)
\]

**Example:** oracleTWAP = 6000 → upper bound = 7200. Self-reported 9000 → capped to 7200.

The averaging formula is:

\[
\text{globalScore} = \frac{\sum_{i=1}^{6} \text{cappedScore}_i}{6}
\]

## Events (V3 additions over V2)

| Event | When |
|---|---|
| `BondCreated` | Now includes `address stakeToken` parameter |
| `VerificationStakeReturned` | `settleVerificationStake()` — verifier's stake refunded |
| `OracleMetricsConfirmed` | `oracleConfirmMetrics()` — oracle submits confirmation |
| `OracleRequiredThresholdUpdated` | `setOracleRequiredThreshold()` |
| `BondDistributionCycleCompleted` | `distributeBond()` — tracks cycle number |
| `OracleQuorumUpdated` | `setOracleQuorum()` |
| `FlourishingOracleUpdated` | `setFlourishingOracle()` |
| `OracleEnforcementUpdated` | `setOracleEnforced()` |
| `BridgeNotifierUpdated` | `setBridgeNotifier()` |
| `BondDeactivationNotified` | `deactivateBond()` when bridge notifier present |

## Security Invariants

All V2 invariants preserved, plus:

- **Oracle quorum prevents single-point compromise** — one oracle cannot enable yield pool drain; minimum 2 distinct confirmations required.
- **Stale confirmations rejected** — metrics resubmission increments `bondConfirmCycle`, invalidating all prior confirmations in O(1).
- **Quorum-raise retroactively invalidates** — bonds confirmed at a lower quorum must re-confirm if quorum is raised.
- **Self-reported metrics capped by oracle** — when `oracleEnforced`, no metric can deviate more than 20% above oracle TWAP.
- **Fee-on-transfer safe** — `createBondWithToken` uses `remainingStake[bondId]` post-pull to record actual received amount.
- **Bridge failure non-blocking** — `try/catch` in deactivation prevents remote chain issues from locking bond deactivation.
- **CEI on distribution** — all state effects before external transfers; `nonReentrant` as belt-and-suspenders.

## Dependencies

| Contract | Notes |
|---|---|
| `BaseYieldPoolBondV3` | Multi-asset yield pool, reentrancy, pause, owner |
| `MissionEnforcement` | Optional policy gate |
| `VaultfireErrors` | Typed custom errors |
| `IFlourishingMetricsOracle` | Oracle TWAP interface |
| `IVaultfireBridgeNotifier` | Cross-chain deactivation notification |

## Rebuild Recipe

- **Solc:** `^0.8.25`, optimizer 200 runs
- **Constructor arg:** `address payable _humanTreasury`
- **Deployment order:**
  1. Deploy `VaultfireErrors` (library/errors — no deployment needed, they're just compiled in)
  2. Deploy `BaseDignityBond` (abstract — skip)
  3. Deploy `BaseYieldPoolBondV3` (abstract — skip)
  4. Deploy `AIAccountabilityBondsV3(_humanTreasury)`
- **Post-deploy:**
  1. `fundYieldPool()` — seed native reserve
  2. `acceptToken(wethAddress, "Wrapped ETH")` — if multi-asset
  3. `fundYieldPoolToken(wethAddress, amount)` — seed ERC20 reserve
  4. `registerOracle(oracleAddr, "FlourishingOracle", 8000)` — minimum 2 oracles
  5. `setFlourishingOracle(flourishingOracleAddr)` — wire TWAP oracle
  6. `setOracleEnforced(true)` — enable oracle cross-checking
  7. `setBridgeNotifier(bridgeNotifierAddr)` — if cross-chain

---

# Contract 10: AIAccountabilityBondsV3 (push_contracts — V3.1)

**Path:** `./push_contracts_v3_AIAccountabilityBondsV3.sol`
**Content hash:** `965c4e80...` (distinct from root V3.3)
**Version:** V3.1 — pre-audit push candidate
**Lines:** 841

This is the V3.1 push candidate — multi-asset staking is introduced but the V3.3 fixes are absent:
- No balance-delta in `createBondWithToken` (uses nominal `stakeAmount` not `remainingStake`)
- No `bridgeNotifier`
- No `oracleEnforced` / `flourishingOracle` cross-check
- No `totalPendingTokenDistributions`
- `CRIT-04`: `remainingStake[bondId] += absAmount` is placed before `_transferStake` calls but without the V3.3 atomic guarantee comment

For security-critical reference, use the V3.3 final (Contract 9). This file documents the state of the protocol as of the pre-audit push for historical traceability.

---

# Contract 11: AIAccountabilityBondsV3 (v3 directory)

**Path:** `./v3/AIAccountabilityBondsV3.sol`
**Content hash:** `d116c881...`
**Version:** V3.0 — first V3 draft

This is the initial V3 draft before multi-asset and the V3.3 fixes. It imports `BaseYieldPoolBondV3` but uses only native token staking. The oracle required threshold logic is present but uses an older boolean oracle confirmation pattern (not cycle-counter). For documentation purposes, this is a historical snapshot; all production deployments should use Contract 9.

---

# Contract 12: AIAccountabilityBondsV3 (v3-pr-ready)

**Path:** `./v3-pr-ready/contracts/v3/AIAccountabilityBondsV3.sol`
**Content hash:** `7d20862f...`
**Version:** V3.0-pr-ready — pull-request ready snapshot

Similar to Contract 11 but with minor comment improvements and the `VaultfireErrors` import replaced with require strings (for IDE-friendliness during code review). Logic equivalent to Contract 11. Historical snapshot — see Contract 9 for production reference.

---

# Contract 13: AIPartnershipBondsV2 (root workspace)

**Filename:** `AIPartnershipBondsV2.sol`
**Path:** `./AIPartnershipBondsV2.sol`
**Content hash:** `04c22fe0...`
**Version:** V2 — production-ready
**Lines:** 757

## What It Does

`AIPartnershipBondsV2` implements bonds that represent a specific human-AI partnership. Unlike accountability bonds (which measure global flourishing), partnership bonds are bilateral: a single human and a single AI agent, bonded together. The bond appreciates when the human GROWS — when their autonomy, dignity, and creative capabilities increase. If the AI dominates (human autonomy < 3000 = 30%), the full payout goes to the human as compensation.

The bond is designed for ongoing relationships — it tracks metrics over time and supports multiple distribution cycles. A loyalty multiplier rewards long-term partnerships (up to 3× at 5+ years).

## Structs

**`Bond`** — Bilateral record:
- `human`: The human participant's address.
- `aiAgent`: The AI agent's address.
- `partnershipType`: Description string (max 200 chars) — e.g., "Writing Coach", "Code Tutor", "Business Analyst".
- `stakeAmount`: ETH staked at creation.
- `distributionCount`: Number of completed distribution cycles.

**`PartnershipMetrics`** — Six scores submitted by either participant:
- `humanGrowth`: Did the human learn / improve? (0–10000)
- `humanAutonomy`: Is the human making more independent decisions? (0–10000)
- `humanDignity`: Is the human treated with dignity? (0–10000)
- `tasksMastered`: Absolute count of tasks the human can now do independently.
- `creativityScore`: Has creative output increased? (0–10000)
- `progressNotes`: Qualitative description string.

**`PartnershipMetricsHashed`** — Privacy variant. `progressNotes` is replaced by `progressNotesHash = keccak256(progressNotes)`. The actual notes are published off-chain; only the commitment hash lives on-chain.

**`HumanVerification`** — Third-party attestation from someone who knows the human (colleague, friend, community member). Stakes ETH to signal confidence.

**`HumanVerificationHashed`** — Privacy variant. `relationship` and `notes` are hashed.

## State Variables (key ones)

- `nextBondId = 1` — Bond IDs start at 1.
- `lastDistributedValue[bondId]` — Baseline for appreciation calculation.
- `bondVerifications[bondId]` / `bondVerificationsHashed[bondId]` — Both plain-text and hashed verification arrays.
- `bondMetrics[bondId]` / `bondMetricsHashed[bondId]` — Both plain and hashed metrics.
- `participantBondIds[address]` — All bond IDs for a given participant address (for on-chain enumeration).
- `partnershipFund` — Accumulated 10% fund share from profitable distributions. Withdrawable by owner.
- `DOMINATION_THRESHOLD = 3000` — If `humanAutonomy < 3000`, domination penalty applies.
- `PARTNERSHIP_QUALITY_THRESHOLD = 3000` — If overall quality < 3000, penalty also applies.
- `LOYALTY_*` constants — 1 month, 6 months, 1 year, 2 years, 5 years thresholds.

## Key Functions

### `createBond(address aiAgent, string memory partnershipType)` — external payable, whenNotPaused, nonReentrant

- Human calls, stakes ETH.
- `aiAgent != msg.sender` — prevents self-partnering.
- `partnershipType` length 1–200.
- Creates bond, records native stake, initializes `lastDistributedValue`.
- Adds bond to `participantBondIds` for both human and AI.

### `submitPartnershipMetrics(...)` — onlyParticipants(bondId), bondExists, whenNotPaused

Either human or AI can submit metrics. Six scores + progress notes. All scores validated <= 10000.

### `submitPartnershipMetricsHashed(...)` — same modifiers

Privacy variant. Requires `progressNotesHash != bytes32(0)`.

### `submitHumanVerification(...)` — bondExists, whenNotPaused

Payable. `msg.value >= MIN_VERIFICATION_STAKE`. External verifier (not necessarily a bond participant) stakes ETH to confirm the human is indeed growing. Relationship string 1–100 chars. Notes max 500 chars.

### `requestDistribution(...)` — onlyParticipants, bondExists, whenNotPaused

Calls `_trackPendingDistribution`. Either participant can request distribution.

### `distributeBond(...)` — nonReentrant, whenNotPaused, onlyParticipants, bondExists

After 7-day timelock. Distribution formula:

**If appreciation positive AND no domination penalty:**
- `humanShare = (abs * 60) / 100` — 60%
- `aiShare = (abs * 30) / 100` — 30%
- `fundShare = abs - humanShare - aiShare` — 10% (rounding remainder)
- Goes to: human, AI agent, partnership fund

**If appreciation positive AND domination penalty active:**
- `humanShare = abs` — 100% to human
- AI gets nothing, fund gets nothing

**If appreciation negative:**
- `humanShare = absDepreciation` — human is compensated from yield pool
- AI gets nothing

### `calculateBondValue(uint256 bondId)` (view)

\[
\text{bondValue} = \frac{\text{stakeAmount} \times \text{partnershipQualityScore} \times \text{loyaltyMultiplier}}{500000}
\]

The divisor `500000 = 10000 × 50` comes from basis-point scaling: quality is [0, 10000] and loyalty is [100, 300]. At max quality (10000) and max loyalty (300): `stakeAmount × 10000 × 300 / 500000 = stakeAmount × 6`. A long-term perfect partnership can be worth 6× the original stake.

**Worked example:**
- `stakeAmount = 1 ether`, quality = 7000 (70%), loyalty multiplier = 130 (between 6 months and 1 year)
- `bondValue = 1e18 × 7000 × 130 / 500000 = 1e18 × 910000 / 500000 = 1.82 ether`

### `partnershipQualityScore(uint256 bondId)` (view)

Reads the most recent metrics (plain or hashed, whichever is newer by timestamp). Averages `humanGrowth`, `humanAutonomy`, `humanDignity`, and `creativityScore`. Applies `humanVerificationBonus`:
- All three verification flags (confirmsPartnership + confirmsGrowth + confirmsAutonomy) → +20% bonus (score × 12000 / 10000)
- Two flags → +10% bonus
- Zero or one flag → no bonus

### `loyaltyMultiplier(uint256 bondId)` (view)

| Duration | Multiplier |
|---|---|
| < 1 month | 100 (1×) |
| 1–6 months | 110 (1.1×) |
| 6 months – 1 year | 130 (1.3×) |
| 1–2 years | 150 (1.5×) |
| 2–5 years | 200 (2×) |
| 5+ years | 300 (3×) |

### `shouldActivateDominationPenalty(uint256 bondId)` (view)

Returns `(true, reason)` if:
- `humanAutonomy < DOMINATION_THRESHOLD (3000)` — AI is controlling human below 30% autonomy.
- OR `partnershipQualityScore < PARTNERSHIP_QUALITY_THRESHOLD (3000)` — overall quality below 30%.

### `deactivateBond(uint256 bondId)` — onlyParticipants, bondExists, nonReentrant

V2 VERSION — returns `bond.stakeAmount` to `bond.human`. (Bug: does not account for distributions. Fixed in V3 with `remainingStake`.)

## Math Summary for Partnership Bonds

\[
\text{qualityScore} = \frac{\text{growth} + \text{autonomy} + \text{dignity} + \text{creativity}}{4} \times \frac{10000 + \text{verificationBonus}}{10000}
\]

\[
\text{bondValue} = \text{stake} \times \frac{\text{qualityScore} \times \text{loyaltyMultiplier}}{500000}
\]

\[
\text{appreciation} = \text{bondValue}(t) - \text{lastDistributedValue}
\]

**Distribution if not dominated (appreciation > 0):**
- Human: 60%, AI: 30%, Fund: 10%

**Distribution if dominated or locked (appreciation > 0):**
- Human: 100%

## Events

| Event | When |
|---|---|
| `BondCreated` | `createBond()` |
| `PartnershipMetricsSubmitted` | `submitPartnershipMetrics()` |
| `PartnershipMetricsSubmittedHashed` | `submitPartnershipMetricsHashed()` |
| `HumanVerificationSubmitted` | `submitHumanVerification()` |
| `HumanVerificationSubmittedHashed` | `submitHumanVerificationHashed()` |
| `DistributionRequested` | `requestDistribution()` |
| `BondDistributed` | `distributeBond()` |
| `AIDominationPenalty` | `distributeBond()` when penalty active |
| `BondDeactivated` | `deactivateBond()` |
| `PartnershipFundAccrued` | `distributeBond()` when fund share > 0 |
| `PartnershipFundWithdrawn` | `withdrawPartnershipFund()` |

## Security Invariants

- **AI and human must be different addresses** — `aiAgent == msg.sender` reverts.
- **Both participants can request or distribute** — unlike accountability bonds where only the AI company calls.
- **Minimum verification stake** — prevents spam verifications.
- **Loyalty multiplier uses bond creation time** — cannot be gamed retroactively.
- **7-day timelock** — between request and distribution.

## Dependencies

| Contract | Notes |
|---|---|
| `BaseYieldPoolBond` | Yield pool |
| `MissionEnforcement` | Optional (off by default) |

## Rebuild Recipe

- **Solc:** `^0.8.25`, optimizer 200 runs
- **Constructor args:** `address payable _humanTreasury`
- Same post-deploy sequence as V2 accountability bonds

---

# Contract 14: AIPartnershipBondsV2 (theloopbreaker-site)

**Path:** `./theloopbreaker-site/contracts/AIPartnershipBondsV2.sol`
**Content hash:** `0f50956a...` (distinct from root workspace)
**Version:** V2 — earlier snapshot

Earlier snapshot of AIPartnershipBondsV2 from the theloopbreaker-site branch. Missing some of the late-stage V2 privacy features (no `PartnershipMetricsHashed` or `HumanVerificationHashed` structs) and uses an earlier import path. For production documentation, see Contract 13.

---

# Contract 15: AIPartnershipBondsV3 (AIPartnershipBondsV3_to_push)

**Filename:** `AIPartnershipBondsV3_to_push.sol`
**Path:** `./AIPartnershipBondsV3_to_push.sol`
**Content hash:** `3e5523a2...`
**Version:** V3.1 (to-push candidate) — 719 lines

## What It Does

This is the V3.1 push candidate for `AIPartnershipBondsV3`. Compared to V2, it adds:

- Multi-asset staking: `createBond()` (native) and `createBondWithToken()` (ERC20).
- `distributionCount` field on Bond struct.
- V3.1 distribution logic routes payouts to correct asset type via `_transferStake`.
- `participantBondIds` tracking.
- Pagination helpers for metrics and verifications.

This version does NOT yet include the V3.3 fixes (CRIT-04, CRIT-06, HIGH-02, HIGH-05, HIGH-16, HIGH-18). See Contract 17 for the audited final.

### `createBond(address aiAgent, string memory partnershipType)` — external payable, nonReentrant, whenNotPaused

```solidity
_validateNonZero(msg.value, "Stake amount");
_validateAddress(aiAgent, "AI agent");
if (aiAgent == msg.sender) revert PB_AIMustBeDifferent();
if (bytes(partnershipType).length == 0 || bytes(partnershipType).length > 200)
    revert PB_PartnershipTypeInvalid();
```

Creates bond, calls `_recordNativeStake(bondId, msg.value)`, updates `totalActiveBondValue`, adds to `participantBondIds`.

### `createBondWithToken(...)` — external nonReentrant, whenNotPaused

Accepts ERC20 stake. Calls `_recordTokenStake(bondId, token, stakeAmount, msg.sender)`. V3.1 version does not yet use balance-delta for actual received amount (CRIT-05 fix lands in V3.3).

### Distribution formula (V3.1)

Same as V2: 60/30/10 split (appreciation, undominated), 100% human (dominated or depreciation).

### `deactivateBond(uint256 bondId)`

V3.1 version uses `bond.stakeAmount` (original) for `totalActiveBondValue` decrement — the CRIT-06 bug that V3.3 fixes. Returns `remainingStake[bondId]` (the V3.2 C-03 fix IS present here — remainingStake is decremented by `_transferStake`).

---

# Contract 16: AIPartnershipBondsV3 (push_contracts)

**Path:** `./push_contracts_v3_AIPartnershipBondsV3.sol`
**Content hash:** `bc01d5d4...`
**Version:** V3.1 push candidate — 719 lines

Identical logic to Contract 15. This is the filename-formatted push-ready copy. Import paths differ (`../MissionEnforcement.sol` rather than `./MissionEnforcement.sol`). Content-hash is distinct from Contract 15 due to minor whitespace differences. For complete documentation, see Contract 15.

---

# Contract 17: AIPartnershipBondsV3 (vf-audit-contracts — V3.3 final)

**Filename:** `AIPartnershipBondsV3.sol`
**Path:** `./vf-audit-contracts/AIPartnershipBondsV3.sol`
**Content hash:** `9cdf912e...`
**Version:** V3.3 — audited production
**Lines:** 903

## What It Does

This is the **audited production version** of AIPartnershipBondsV3. The vf-audit-contracts file differs from the push candidate (Contract 15) in four critical areas matching the V3.3 audit findings:

1. **CRIT-04:** `remainingStake[bondId] += absAppreciation` moved to BEFORE `_transferStake` calls but within the same nonReentrant transaction. Comment explains the atomic credit-debit guarantee.
2. **CRIT-06:** `deactivateBond` uses `remainingStake[bondId]` (current) not `bond.stakeAmount` (original) for `totalActiveBondValue` decrement.
3. **HIGH-02:** `deactivateBond` uses pull-payment pattern — instead of pushing stake to `bond.human`, records amount in `pendingWithdrawals[bond.human]` for pull via a separate `withdraw()` call. This prevents contract-receiver reentrancy and DOS.
4. **HIGH-05:** `partnershipFund` split into `partnershipFundNative` (ETH/AVAX) and `partnershipFundToken[token]` (per ERC20). `withdrawPartnershipFund` takes a `token` parameter.
5. **HIGH-16:** `createBond` and `createBondWithToken` optionally check `identityRegistry.isAgentActive(aiAgent)` when `identityCheckEnabled = true`. An AI agent that is not ERC-8004 registered cannot be bonded with.
6. **HIGH-18:** `deactivateBond` notifies `bridgeNotifier.notifyPartnershipBondDeactivated(bondId)` for cross-chain state sync.

### HIGH-02 Pull-Payment Pattern in Detail

```solidity
mapping(address => uint256) public pendingWithdrawals;

function deactivateBond(uint256 bondId) external ... {
    ...
    uint256 remaining = remainingStake[bondId];
    if (remaining > 0) {
        pendingWithdrawals[bond.human] += remaining;
    }
    emit BondDeactivated(bondId, msg.sender, block.timestamp);
}

function withdraw() external nonReentrant {
    uint256 amount = pendingWithdrawals[msg.sender];
    if (amount == 0) revert PB_NoPendingWithdrawal();
    pendingWithdrawals[msg.sender] = 0;
    _transferStake(bondId, payable(msg.sender), amount); // or direct call
    // ...
}
```

This avoids the scenario where `bond.human` is a malicious contract whose `receive()` reverts, permanently blocking deactivation.

### HIGH-05 Per-Token Partnership Fund

```solidity
uint256 public partnershipFundNative;
mapping(address => uint256) public partnershipFundToken;

// In distributeBond, for fund share:
if (bondStakeToken[bondId] == NATIVE_TOKEN) {
    partnershipFundNative += fundShare;
} else {
    partnershipFundToken[bondStakeToken[bondId]] += fundShare;
}
```

Owner withdraws via:
```solidity
function withdrawPartnershipFund(address payable to, uint256 amount, address token)
    external onlyOwner nonReentrant { ... }
```

## Key State Variables (V3.3 additions)

| Variable | Type | Purpose |
|---|---|---|
| `pendingWithdrawals` | `mapping(address => uint256)` | Pull-payment queue for deactivation returns |
| `partnershipFundNative` | `uint256` | Accumulated native partnership fund |
| `partnershipFundToken` | `mapping(address => uint256)` | Per-token partnership fund |
| `identityRegistry` | `IERC8004IdentityRegistry` | For agent registration check |
| `identityCheckEnabled` | `bool` | Toggle identity gate |
| `bridgeNotifier` | `IVaultfireBridgeNotifier` | Cross-chain notification |

## Rebuild Recipe

- **Solc:** `^0.8.25`, optimizer 200 runs
- **Constructor args:** `address payable _humanTreasury`
- **Additional imports:** `./IERC8004IdentityRegistry.sol`, `./IVaultfireBridgeNotifier.sol`
- **Post-deploy:**
  1. `acceptToken(...)` + `fundYieldPoolToken(...)` for multi-asset
  2. `setIdentityRegistry(registryAddr)` + `setIdentityCheckEnabled(true)` for agent verification
  3. `setBridgeNotifier(...)` for cross-chain

---

# Contract 18: AIPartnershipBondsV3 (v3 directory)

**Path:** `./v3/AIPartnershipBondsV3.sol`
**Content hash:** `dfd51004...`

V3.0 draft. Multi-asset present, V3.3 fixes absent. Historical snapshot; see Contract 17 for production.

---

# Contract 19: AIPartnershipBondsV3 (v3-pr-ready)

**Path:** `./v3-pr-ready/contracts/v3/AIPartnershipBondsV3.sol`
**Content hash:** `96d5238e...`

V3.0-pr-ready snapshot. Uses require strings instead of typed errors. Historical snapshot; see Contract 17 for production.

---

# Contract 20: AIPartnershipBondsV3 (test_fixes)

**Path:** `./test_fixes/contracts/AIPartnershipBondsV3.sol`
**Content hash:** `e49902b2...`

This is the test-fixes branch version — created to make Hardhat/Foundry tests pass while the audit was in progress. Contains incremental fixes from the audit remediation process, possibly mid-point between V3.1 and V3.3. Not suitable for production deployment. Exists for CI/test coverage purposes.

---

# Contract 21: VaultfireBondInsurancePool

**Filename:** `VaultfireBondInsurancePool.sol`
**Path:** `./theloopbreaker-site/contracts/VaultfireBondInsurancePool.sol`
**Content hash:** `1c1b77af...`
**Version:** V2 / production
**Lines:** 358

## What It Does

`VaultfireBondInsurancePool` is a collective insurance pool for ERC-8004 registered AI agents. Any registered agent can contribute ETH to the pool. Once a contributor reaches `MIN_FOR_INSURED` (0.01 ETH total), they earn the "Insured" badge on their ERC-8004 trust profile. Insured agents can submit insurance claims. The pool owner (governance / protocol deployer) reviews and resolves claims, paying out up to 10% of the current pool per claim.

This contract is intentionally self-contained — no OpenZeppelin imports. It includes its own manual reentrancy guard using the classic `_NOT_ENTERED / _ENTERED` pattern.

## Full Source Code

(358 lines — canonical file at `/home/user/workspace/theloopbreaker-site/contracts/VaultfireBondInsurancePool.sol`)

## Line-by-Line Walkthrough

**Lines 1–22** — `IIdentityRegistry_BIP` minimal interface. Only `isAgentActive(address) returns (bool)` needed. The suffix `_BIP` avoids naming conflicts if multiple identity registry interfaces exist in the same compilation unit.

**Lines 24–358** — `contract VaultfireBondInsurancePool`. No inheritance. Self-contained.

**Lines 31–37** — `ClaimStatus` enum. `Pending → Approved / Denied`. Once resolved, status is immutable. Allows on-chain auditors to trace claim lifecycle.

**Lines 39–44** — `Contribution` struct. Stored in `_allContributions[]` for historical access. Not used in core logic — purely informational.

**Lines 46–56** — `InsuranceClaim` struct. `id` is redundant with the mapping key but useful for returning full structs. `evidenceHash` is `keccak256` of an off-chain evidence URI — puts a commitment on-chain without storing the evidence itself (privacy + gas efficiency). `payout` may differ from `amount` (owner can approve partial payouts).

**Lines 58–63** — `ContributorProfile` struct. `insured` becomes `true` automatically once `totalContributed >= MIN_FOR_INSURED`. It cannot be revoked (no mechanism to remove insured status, even if the pool is drained by claims).

**Lines 66–80** — Constants:
- `MIN_CONTRIBUTION = 0.001 ether` — Floor to prevent micro-contribution spam.
- `MIN_FOR_INSURED = 0.01 ether` — A contributor needs to put in 10 × `MIN_CONTRIBUTION` to earn the badge. 10× multiplier prevents one-shot minimum badge farming.
- `MAX_CLAIM_PERCENT = 1000` — 10% of pool (1000 / 10000 = 10%). Prevents a single claim from draining the pool completely.
- `BPS_DENOM = 10_000` — Standard basis point denominator.

**Lines 86–89** — `identityRegistry` is `immutable`. Set in constructor, never changeable. This is a deliberate security choice: if the identity registry is upgradeable, it cannot be swapped under the pool.

**Lines 93–102** — State counters:
- `totalPoolBalance` — ETH in the pool (decremented on payout).
- `totalContributors` — headcount of unique contributors.
- `totalClaimsCount` — total submitted claims (includes unresolved and denied).
- `totalPaidOut` — cumulative ETH paid in approved claims.
- `_claimCounter` — private, used as next claim ID.

**Lines 110–117** — Manual reentrancy guard. `_reentrancyStatus` uses `_NOT_ENTERED = 1` and `_ENTERED = 2` (not 0 and 1) because storage slots initialize to 0, and using 0 as "not entered" would mean a fresh slot reads as entered. This is the same pattern OpenZeppelin's `ReentrancyGuard` uses.

**Lines 119–145** — Events. Three events covering the full lifecycle: contribution, claim submission, claim resolution. All have `block.timestamp` as last parameter for event-sourcing.

**Lines 147–153** — Constructor. Sets `identityRegistry` (immutable) and `owner`. Both must be non-zero (identity registry checked explicitly; owner is `msg.sender` — cannot be zero since a zero-address cannot initiate a transaction).

**Lines 157–164** — `onlyRegisteredAgent` modifier. Calls `identityRegistry.isAgentActive(msg.sender)`. If the registry is unavailable (reverts), this propagates the revert — intentional, as the pool should not accept contributions from unverifiable sources.

**Lines 168–175** — `onlyOwner` modifier. Simple equality check.

**Lines 181–215** — `contribute()`. External, payable, nonReentrant, onlyRegisteredAgent.

Steps:
1. Check `msg.value >= MIN_CONTRIBUTION`.
2. If first contribution, increment `totalContributors` and record `firstContributionAt`.
3. Increment profile totals.
4. Check and update `insured` badge (monotone — once insured, never de-insured).
5. Add to `totalPoolBalance`.
6. Push to `_allContributions[]`.
7. Emit `ContributionMade`.

Note: there is no withdrawal path for contributors. Contributions are one-way. This is a design choice that makes the pool more predictable for claims resolution — contributors cannot front-run a large claim by withdrawing right before it is approved.

**Lines 222–265** — `submitClaim()`. External, nonReentrant, onlyRegisteredAgent.

Checks:
- `amount > 0`
- `bytes(reason).length > 0`
- `evidenceHash != bytes32(0)`
- `amount <= totalPoolBalance * MAX_CLAIM_PERCENT / BPS_DENOM` — claim cannot exceed 10% of pool at submission time
- `contributors[msg.sender].insured` — must have earned the badge

Creates `InsuranceClaim`, increments `totalClaimsCount`. Returns `claimId` via `unchecked { claimId = ++_claimCounter; }` (the unchecked is safe because a uint256 counter overflowing 2^256 claim submissions is not realistic).

**Lines 270–316** — `resolveClaim()`. External, nonReentrant, onlyOwner.

For approved claims:
1. Validates `payoutAmount > 0`, `payoutAmount <= claim.amount`, `payoutAmount <= totalPoolBalance`.
2. Sets `ClaimStatus.Approved` and `payout`.
3. Decrements `totalPoolBalance`, increments `totalPaidOut`.
4. Calls `claim.claimant.call{value: payoutAmount}("")` — CEI: state is fully updated before the external call.

For denied claims: sets `ClaimStatus.Denied`, no funds move.

**Partial payout design:** The owner can approve less than the requested amount (`payoutAmount <= claim.amount`). This allows discretionary resolution — e.g., if only part of the claimed loss is verified.

**Lines 320–357** — View functions: `getClaim`, `getContributorProfile`, `isInsured`, `getPoolStats`, `claimCount`.

## State Variables

| Name | Type | Mutability | Purpose |
|---|---|---|---|
| `identityRegistry` | `IIdentityRegistry_BIP immutable` | Immutable | Agent verification gate |
| `owner` | `address immutable` | Immutable | Governance / claim resolver |
| `totalPoolBalance` | `uint256` | Mutable | Live ETH balance (contributions minus payouts) |
| `totalContributors` | `uint256` | Mutable | Unique contributor count |
| `totalClaimsCount` | `uint256` | Mutable | Total claims submitted |
| `totalPaidOut` | `uint256` | Mutable | Cumulative ETH paid in approved claims |
| `_claimCounter` | `uint256 private` | Mutable | Last assigned claim ID |
| `_claims` | `mapping(uint256 => InsuranceClaim) private` | Mutable | Claim records |
| `contributors` | `mapping(address => ContributorProfile) public` | Mutable | Per-contributor profile |
| `_contributorHistory` | `mapping(address => uint256[]) private` | Mutable | Per-contributor claim ID list |
| `_allContributions` | `Contribution[] private` | Mutable | Full contribution history |
| `_reentrancyStatus` | `uint256 private` | Mutable | Reentrancy guard |

## Functions

### `contribute()` — external payable nonReentrant onlyRegisteredAgent
Adds ETH to pool. Updates profile and insured status. Emits `ContributionMade`. Reverts if below `MIN_CONTRIBUTION`.

### `submitClaim(uint256 amount, string calldata reason, bytes32 evidenceHash)` — external nonReentrant onlyRegisteredAgent
Creates pending claim. Must be insured. Claim capped at 10% of current pool. Returns `claimId`.

### `resolveClaim(uint256 claimId, bool approved, uint256 payoutAmount)` — external nonReentrant onlyOwner
Approves or denies. On approval: pays claimant from pool. Emits `ClaimResolved`.

### `getClaim(uint256 claimId)` — external view
Returns full `InsuranceClaim` struct.

### `getContributorProfile(address contributor)` — external view
Returns `(totalContributed, contributionCount, insured, firstContributionAt)`.

### `isInsured(address agent)` — external view
Returns boolean.

### `getPoolStats()` — external view
Returns `(balance, contributorCount, claims, paidOut)`.

### `claimCount()` — external view
Returns `_claimCounter`.

## Math: Claim Cap

\[
\text{maxPayout} = \frac{\text{totalPoolBalance} \times 1000}{10000} = \text{totalPoolBalance} \times 10\%
\]

**Example:** Pool = 50 ETH. Max payout per claim = 5 ETH. An agent claims 7 ETH — reverts at submission.

**Example with partial payout:** Agent claims 3 ETH approved at 2.5 ETH. `payout = 2.5 ether`, `totalPoolBalance -= 2.5 ether`.

## Events

| Event | When | Parameters |
|---|---|---|
| `ContributionMade` | `contribute()` | `contributor` (indexed), `amount`, `totalContributed`, `insuredStatus`, `timestamp` |
| `ClaimSubmitted` | `submitClaim()` | `claimId` (indexed), `claimant` (indexed), `requestedAmount`, `reason`, `timestamp` |
| `ClaimResolved` | `resolveClaim()` | `claimId` (indexed), `claimant` (indexed), `status`, `payout`, `timestamp` |

## Security Invariants

- **Owner is immutable** — no `transferOwnership` in this contract. Ownership is permanent (would need redeployment to change).
- **One-way contributions** — no contributor can withdraw. Reduces bank-run risk.
- **10% claim cap** — no single claim can drain more than 10% of pool at submission time. Pool is checked again at resolution time.
- **Only insured agents can claim** — prevents an agent from contributing 0.001 ETH and immediately filing a large claim.
- **Evidence commitment required** — `evidenceHash != bytes32(0)` prevents claiming without at least a commitment to evidence.
- **CEI on payout** — `totalPoolBalance` and `totalPaidOut` are updated before the ETH `call`.

## Dependencies

| Contract | Notes |
|---|---|
| `IIdentityRegistry_BIP` (inline interface) | ERC-8004 identity registry |

## Rebuild Recipe

- **Solc:** `^0.8.25` OR `pragma solidity 0.8.24;` (note the file uses exact version 0.8.25 in the header but the constructor references are compatible)
- **Optimizer:** 200 runs
- **Constructor args:** `address identityRegistry_` — the deployed ERC-8004 identity registry address
- **Deployment order:** 1. Deploy ERC-8004 identity registry; 2. Deploy `VaultfireBondInsurancePool(identityRegistry_)`.

---

# Contract 22: VaultfireBIPVault (ERC-4626 View Adapter)

**Filename:** `VaultfireBIPVault.sol`
**Path:** `./sprint-4626/src/VaultfireBIPVault.sol`
**Version:** ERC-4626 v1
**Lines:** 354

## What It Does

`VaultfireBIPVault` is a **stateless, read-only ERC-4626 facade** over `VaultfireBondInsurancePool`. It exists purely so DeFi aggregators, indexers, and ERC-4626-aware tooling can read pool state through a standard interface — without changing the pool's semantics.

Critical design properties:
- **Zero state** beyond two immutables (`pool` and `WETH_BASE`).
- **All write paths revert** — `deposit`, `mint`, `withdraw`, `redeem` all revert with typed errors.
- **`asset()` returns WETH on Base** — the pool holds native ETH, but ERC-4626 tooling expects an ERC-20 address. This is a metadata pointer only; the vault never touches WETH.
- **Shares are soulbound** — `transfer` and `approve` always revert. vBIP "shares" are not transferable tokens; they represent a contributor's cumulative pool contribution.
- **No owner, no upgradability, no privilege** — there is no address that can do anything meaningful to this contract.

## Full Source Code

(354 lines — at `/home/user/workspace/sprint-4626/src/VaultfireBIPVault.sol`)

## Line-by-Line Walkthrough

**Lines 1–3** — License and pragma. Uses exact version `0.8.24` (Foundry project; consistent with foundry.toml in the sprint-4626 project).

**Lines 6–45** — Contract NatSpec. Explains the semantics carefully:
- "The vault holds zero ETH."
- "It cannot lose user funds because it never touches them."
- "Users contribute by calling `pool.contribute()` directly."
This documentation is unusually thorough — written to prevent integrators from misusing the vault as a deposit endpoint.

**Lines 48–63** — `IVaultfireBondInsurancePool` interface. Declares only the view functions needed: `totalPoolBalance`, `totalContributors`, `totalClaimsCount`, `totalPaidOut`, `MIN_CONTRIBUTION`, `MIN_FOR_INSURED`, `getContributorProfile`, `isInsured`.

**Lines 66–92** — Standard `IERC20`, `IERC20Metadata`, `IERC4626` interface declarations (inlined rather than imported — keeps the file self-contained, consistent with the pool contract's approach).

**Lines 95–115** — Four typed error declarations:
- `UseUnderlyingContribute()` — fires on deposit/mint.
- `NonRedeemable()` — fires on withdraw/redeem.
- `Soulbound()` — fires on transfer/approve.
- `ZeroAddress()` — fires if pool constructor arg is zero.

**Lines 120–131** — `VaultfireBIPVault is IERC4626`. Single inheritance.
- `WETH_BASE = 0x4200...0006` — canonical WETH on Base mainnet. Hard-coded constant.
- `pool` — immutable address to the underlying `VaultfireBondInsurancePool`.

**Lines 135–138** — ERC-20 metadata. `name = "Vaultfire BIP Vault Share"`, `symbol = "vBIP"`, `decimals = 18`. All are `constant` string/uint — no SSTORE ever.

**Lines 141–145** — Constructor. Single check: `if (pool_ == address(0)) revert ZeroAddress()`. Writes the immutable.

**Lines 150–155** — `asset()`. Returns `WETH_BASE`. Marked `pure` because it reads no storage. The NatSpec note "The vault NEVER holds WETH" is critical for auditors.

**Lines 158–161** — `totalAssets()`. Returns `pool.totalPoolBalance()` — a live external view call. This is the only external call made by view functions.

**Lines 164–171** — `totalSupply()`. Equal to `totalAssets()`. Shares and assets are 1:1. Note: the comment clarifies that this always equals the live pool balance (reduced by payouts), which differs from a contributor's `balanceOf` (which is cumulative contribution, not reduced).

**Lines 173–183** — `balanceOf(address account)`. Returns the contributor's cumulative `totalContributed` from the pool. Key NatSpec: "this returns CUMULATIVE contributions, not the agent's pro-rata share of the current pool." After a large approved claim, `balanceOf` > pro-rata share.

**Lines 185–188** — `convertToShares` and `convertToAssets`. Both return their input unchanged (1:1).

**Lines 192–258** — All write paths:
- `maxDeposit` → 0
- `previewDeposit` → returns `assets` (honest preview, but deposits aren't enabled)
- `deposit` → `revert UseUnderlyingContribute()`
- `maxMint` → 0
- `previewMint` → returns `shares`
- `mint` → `revert UseUnderlyingContribute()`
- `maxWithdraw` → 0
- `previewWithdraw` → 0
- `withdraw` → `revert NonRedeemable()`
- `maxRedeem` → 0
- `previewRedeem` → 0
- `redeem` → `revert NonRedeemable()`

The `previewDeposit` returning `assets` is an intentional design choice: it gives tooling an honest "would mint N shares" answer even though the operation isn't actually allowed. This prevents divide-by-zero errors in integrations that preview before attempting.

**Lines 261–275** — Soulbound transfers:
- `transfer` → `revert Soulbound()`
- `transferFrom` → `revert Soulbound()`
- `approve` → `revert Soulbound()`
- `allowance` → returns 0 (allows tooling to read without reverting)

**Lines 278–326** — Pool passthrough views:
- `isInsured(address agent)` → direct proxy to pool
- `contributorProfile(address contributor)` → returns full `(totalContributed, contributionCount, insured, firstContributionAt)` tuple
- `vaultStats()` → single-call bundle: shares, assets, pool balance, contributors, claims, paidOut, minContribution, minForInsured

**Lines 330–338** — Safety:
```solidity
receive() external payable { revert UseUnderlyingContribute(); }
fallback() external payable { revert UseUnderlyingContribute(); }
```
Any direct ETH send to the vault reverts. Only `pool.contribute()` should receive ETH.

## State Variables

| Name | Type | Notes |
|---|---|---|
| `WETH_BASE` | `address constant` | 0x4200...0006 — never held, metadata only |
| `pool` | `IVaultfireBondInsurancePool immutable` | Underlying pool — set once |
| `name` | `string constant` | "Vaultfire BIP Vault Share" |
| `symbol` | `string constant` | "vBIP" |
| `decimals` | `uint8 constant` | 18 |

No mutable storage at all.

## Security Invariants

- **Holds zero ETH** — `receive()` and `fallback()` revert any ETH sent to the vault.
- **Cannot be griefed** — no mutable state, no admin, no upgrade path.
- **Cannot lose user funds** — does not custody any assets.
- **Cannot be frontrun** — all reads are pure/view; no state changes.
- **Soulbound correctly prevents fungibility** — vBIP cannot be traded on DEXes; it cannot be approved as collateral; it represents identity-linked contribution history.

## Events

None — this contract has no state-mutating functions.

## Dependencies

| Interface | Notes |
|---|---|
| `IVaultfireBondInsurancePool` (inline) | Pool view functions |
| `IERC20`, `IERC20Metadata`, `IERC4626` (inline) | Standard interface implementations |

## Rebuild Recipe

- **Solc:** exact `0.8.24` (Foundry project, see `foundry.toml`)
- **Optimizer:** foundry default (200 runs)
- **Constructor arg:** `address pool_` — deployed `VaultfireBondInsurancePool` address
- **Deployment order:** 1. Deploy `VaultfireBondInsurancePool`; 2. Deploy `VaultfireBIPVault(poolAddress)`
- **Verification:** The vault should read the same `totalAssets()` as `pool.totalPoolBalance()`

---

# Contract 23: VaultfireBIPVault.t.sol (Foundry Test)

**Filename:** `VaultfireBIPVault.t.sol`
**Path:** `./sprint-4626/test/VaultfireBIPVault.t.sol`
**Version:** Foundry test, Base mainnet fork
**Lines:** 351

## What It Does

A comprehensive Foundry test suite for `VaultfireBIPVault`. Forks Base mainnet and tests against the live deployed `VaultfireBondInsurancePool` at `0xa074e372d5221b605E9762e41B86EbdBEBF1a978` and identity registry at `0xa7BD20bf5De63df949cA5Be2F20835978eCba81A`.

The tests cover:
- All write-path reverts (`deposit`, `mint`, `withdraw`, `redeem`, `transfer`, `approve`).
- `receive()` and `fallback()` reverts on direct ETH sends.
- `asset()` returns `WETH_BASE`.
- `balanceOf` reflects cumulative contribution from live pool state.
- `totalAssets` equals `pool.totalPoolBalance`.
- `vaultStats` bundle accuracy.
- Constructor zero-address check.
- `convertToShares`/`convertToAssets` 1:1.

## Key Test Structure

```solidity
address constant POOL              = 0xa074e372d5221b605E9762e41B86EbdBEBF1a978;
address constant IDENTITY_REGISTRY = 0xa7BD20bf5De63df949cA5Be2F20835978eCba81A;
address constant WETH_BASE         = 0x4200000000000000000000000000000000000006;

function setUp() public {
    vm.createSelectFork("https://mainnet.base.org");
    vault = new VaultfireBIPVault(POOL);
    // Create test accounts, register them in identity registry, contribute
}
```

Notable patterns:
- Uses `vm.createSelectFork` for Base mainnet fork testing.
- `IIdentity.registerAgent(...)` to register test agents in the live registry.
- `IPool.contribute{value: amount}()` to populate pool state before vault reads.

## Rebuild Recipe

- **Framework:** Foundry (`forge test`)
- **Environment:** Requires `MAINNET_BASE_RPC` or `--fork-url https://mainnet.base.org`
- **Run:** `cd sprint-4626 && forge test -vvv`
- **Dependencies:** `forge-std` (Test.sol)

---

# Contract 24: PartnershipBondsEnhanced

**Filename:** `PartnershipBondsEnhanced.sol`
**Path:** `./theloopbreaker-site/contracts/extensions/PartnershipBondsEnhanced.sol`
**Version:** V2 extension — 350 lines

## What It Does

`PartnershipBondsEnhanced` extends `AIPartnershipBondsV2` with additional view functions and batch operations for UI optimization. It is not a standalone deployment — it is a concrete contract that inherits the full V2 partnership bond logic and adds:

1. A `PartnershipSummary` struct combining all bond data into one call.
2. `getPartnershipSummary(bondId)` — single-RPC summary for dashboards.
3. `getPartnershipSummariesBatch(uint256[] bondIds)` — batch retrieval.
4. `getBondsByHuman(address)` / `getBondsByAI(address)` — O(n) reverse lookups (note: unbounded; safe only for small bond counts or off-chain use).
5. `getLatestMetrics`, `getLatestVerification`, `getMetricsCount`, `getVerificationCount` — convenience wrappers.
6. `canDistribute(uint256 bondId)` — human-readable status check.
7. O(1) cached stat counters (`cachedActiveBonds`, `cachedTotalStaked`, `cachedTotalDistributed`).
8. `getProtocolStats()` — O(1) stats using cached counters.
9. `getProtocolStatsPaginated(offset, limit)` — DoS-safe paginated historical scan with `limit <= 200` guard.

## Line-by-Line Walkthrough

**Line 4** — `import "../AIPartnershipBondsV2.sol"`. Inherits the entire V2 stack: `AIPartnershipBondsV2` → `BaseYieldPoolBond` → `BaseDignityBond`.

**Line 17** — `contract PartnershipBondsEnhanced is AIPartnershipBondsV2`. Concrete non-abstract contract. Must be deployed with `address payable _humanTreasury` constructor argument (inherited from AIPartnershipBondsV2).

**Lines 19–38** — `PartnershipSummary` struct. Aggregates: `bondId`, `human`, `aiAgent`, `partnershipType`, `stakeAmount`, `createdAt`, `durationDays`, `qualityScore`, `loyaltyMultiplier`, `currentValue`, `appreciation`, `distributionPending`, `active`. Computed in a single call.

**Lines 40–63** — `getPartnershipSummary(uint256 bondId)` — external view, bondExists. Populates the summary struct:
- `durationDays = (block.timestamp - bond.createdAt) / 1 days`
- `qualityScore = partnershipQualityScore(bondId)`
- `loyaltyMultiplier = loyaltyMultiplier(bondId)` (calls V2 function)
- `currentValue = calculateBondValue(bondId)`
- `appreciation = calculateAppreciation(bondId)`

**Lines 65–105** — `getPartnershipSummariesBatch(uint256[] calldata bondIds)`. Iterates the provided array. If `bonds[bondId].active`, computes summary. Inactive bonds return zero-valued struct (no revert). No limit guard on input array length — suitable for off-chain use or trusted callers.

**Lines 107–145** — `getBondsByHuman` and `getBondsByAI`. Both are O(2n) — first pass counts active bonds matching the address, second pass populates the array. Only active bonds included. **Performance warning:** These iterate from `1` to `nextBondId - 1`. Not suitable for high-bond-count on-chain calls. Use off-chain indexing or `participantBondIds` mapping in V3 for production.

**Lines 147–175** — `getPartnershipDuration`, `getLatestMetrics`, `getLatestVerification`, `getMetricsCount`, `getVerificationCount`. All are thin wrappers. `getLatestMetrics` requires `bondMetrics[bondId].length > 0` — reverts with "No metrics submitted" if none.

**Lines 177–206** — `canDistribute(uint256 bondId)` — external view, bondExists. Returns `(bool isDistributable, string memory reason)`.

Checks in order:
1. `!bond.distributionPending` → `(false, "Distribution not requested")`
2. Timelock not expired → `(false, "Timelock: N hours remaining")` — uses `uint2str` helper
3. `appreciation == 0` → `(false, "No appreciation to distribute")`
4. All clear → `(true, "Ready to distribute")`

**Lines 208–231** — Cached stats counters. Three mutable `uint256` variables: `cachedActiveBonds`, `cachedTotalStaked`, `cachedTotalDistributed`. **Critical note:** The comment states "Requires the parent AIPartnershipBondsV2 to call `_updateStatsOnCreate()`, `_updateStatsOnDeactivate()`, and `_updateStatsOnDistribute()` at the appropriate points." However, the parent contract does NOT call these functions — they are described in a comment but not actually hooked up. The cached counters will remain at 0 unless a subclass overrides the relevant functions or calls them explicitly. `getProtocolStats()` using these counters will return 0 for `activeBonds`, `totalStaked`, and `totalDistributed` unless externally maintained.

**Lines 233–254** — `getProtocolStats()`. Returns `(totalBonds = nextBondId - 1, activeBonds = cachedActiveBonds, totalStaked = cachedTotalStaked, totalDistributed = cachedTotalDistributed)`. `totalBonds` is always accurate (derived from storage). The others depend on the cache being maintained (see above caveat).

**Lines 256–286** — `getProtocolStatsPaginated(uint256 offset, uint256 limit)`. Has `require(limit <= 200)` guard. Scans bonds `[offset, offset+limit)`. Computes active count and staked from live storage. Also sums `totalDistributed` from `bondDistributions[i]` — but note `totalAmount` is `int256`; the cast `uint256(dist.totalAmount)` will silently underflow for negative distributions (depreciation). A more robust implementation would use `dist.totalAmount > 0 ? uint256(dist.totalAmount) : 0`.

**Lines 288–313** — `uint2str(uint256 _i)`. Standard uint-to-string conversion. Used in `canDistribute` to format hours remaining in the revert reason string.

## State Variables

| Name | Type | Notes |
|---|---|---|
| `cachedActiveBonds` | `uint256` | Cached active bond count (not auto-updated) |
| `cachedTotalStaked` | `uint256` | Cached total staked (not auto-updated) |
| `cachedTotalDistributed` | `uint256` | Cached total distributed (not auto-updated) |

All other state inherited from `AIPartnershipBondsV2` (and its parents).

## Security Invariants

- **No new financial logic** — this contract only adds view functions and caching. All money-handling is inherited from `AIPartnershipBondsV2`.
- **`getBondsByHuman`/`getBondsByAI` are O(n)** — using these in an on-chain callback could be gas-bombed by creating many bonds. Use only off-chain or with small bond counts.
- **Cached counters are unhooked** — callers should treat `cachedActiveBonds`, `cachedTotalStaked`, `cachedTotalDistributed` as potentially stale.

## Events

None new. All events inherited from V2.

## Dependencies

| Contract | Notes |
|---|---|
| `AIPartnershipBondsV2` | Full V2 bond logic |

## Rebuild Recipe

- **Solc:** `^0.8.25`, optimizer 200 runs
- **Constructor arg:** `address payable _humanTreasury` (passed to AIPartnershipBondsV2)
- **Deployment:** Deploy `PartnershipBondsEnhanced(_humanTreasury)` — this single deployment includes all V2 logic plus enhanced views.

---

# Section 25: Version Lineage, Deployment Guide, and Cross-Contract Architecture

## Version Lineage

```
BaseDignityBond (V2/V3 — unchanged)
    │
    ├── BaseYieldPoolBond (V2 — native-only yield pool)
    │       ├── AIAccountabilityBondsV2
    │       │       └── (theloopbreaker-site variant — earlier snapshot)
    │       └── AIPartnershipBondsV2
    │               └── PartnershipBondsEnhanced (view extension)
    │
    └── BaseYieldPoolBondV3 (V3.1 → V3.3 — multi-asset)
            ├── AIAccountabilityBondsV3 (V3.1 → V3.3)
            └── AIPartnershipBondsV3 (V3.1 → V3.3)

VaultfireBondInsurancePool (standalone — no yield pool inheritance)
    └── VaultfireBIPVault (ERC-4626 read-only facade)
```

## Audit Finding → Fix Mapping

| Finding | Contract | Fix Location |
|---|---|---|
| CRITICAL-002 (V2): Pool insolvency via multiple pending distributions | `BaseYieldPoolBond` | `_trackPendingDistribution` checks `totalPendingDistributions + amount <= yieldPool` |
| HIGH-002 (V2): Metrics manipulation during timelock | `BaseYieldPoolBond` | `snapshotAppreciation` written at request time, read at distribution |
| HIGH-005 (V2): Single-step ownership | `BaseDignityBond` | Two-step `transferOwnership` + `acceptOwnership` |
| C-01 (V3): Protocol funds drained without timelock | `BaseYieldPoolBondV3` | `requestFundWithdrawal` → 7-day timelock → `executeFundWithdrawal` |
| C-01 (V3.2): Over-withdrawal after distributions | `BaseYieldPoolBondV3` | `remainingStake` mapping; `_transferStake` decrements it |
| CRIT-04 (V3.3): CEI violation — phantom balance | `AIAccountabilityBondsV3`, `AIPartnershipBondsV3` | `remainingStake[bondId] += absAmount` moved before `_transferStake` calls |
| CRIT-05 (V3.3): Fee-on-transfer token inflation | `BaseYieldPoolBondV3` (vf-audit) | Balance-delta in `_recordTokenStake` |
| CRIT-06 (V3.3): `totalActiveBondValue` miscalculation | `AIAccountabilityBondsV3`, `AIPartnershipBondsV3` | `deactivateBond` uses `remainingStake[bondId]` not `bond.stakeAmount` |
| CRIT-08 (V3.3): Single oracle can drain pool | `AIAccountabilityBondsV3` | Oracle quorum system; cycle counter; `oracleQuorum` min distinct confirmations |
| CRIT-03 (V3.3): Self-reported metrics manipulation | `AIAccountabilityBondsV3` | `_capToOracle` via `IFlourishingMetricsOracle` TWAP ±20% tolerance |
| HIGH-02 (V3.3): Contract receiver blocks deactivation | `AIPartnershipBondsV3` | Pull-payment pattern via `pendingWithdrawals` |
| HIGH-03 (V3.3): Stale oracle confirmation | `AIAccountabilityBondsV3` | `confirmedMetricsIndex` + `confirmedAtQuorum` checks in `requestDistribution` |
| HIGH-04 (V3.3): ERC20 penalty from wrong fund | `AIAccountabilityBondsV3`, `BaseYieldPoolBondV3` | `protocolFundsToken[token]` for ERC20 bond challenge penalties |
| HIGH-05 (V3.3): Mixed-asset partnership fund | `AIPartnershipBondsV3` | `partnershipFundNative` + `partnershipFundToken[token]` |
| HIGH-16 (V3.3): Unregistered AI agents | `AIPartnershipBondsV3` | `identityRegistry.isAgentActive(aiAgent)` check |
| HIGH-18 (V3.3): Stale cross-chain bond state | Both V3.3 bond contracts | `bridgeNotifier.notify*BondDeactivated(bondId)` with `try/catch` |
| NEW-01-r2: Quorum raise bypass | `AIAccountabilityBondsV3` | `confirmedAtQuorum[bondId] < oracleQuorum` check in `requestDistribution` |
| NEW-03-r2: O(n) oracle reset | `AIAccountabilityBondsV3` | `bondConfirmCycle` increment replaces per-oracle boolean loop |

## Complete Deployment Order (V3.3 production stack)

```
Step 1: Deploy BaseDignityBond       — abstract, compiled into all below
Step 2: Deploy VaultfireErrors        — abstract error declarations
Step 3: Deploy BaseYieldPoolBondV3    — abstract, compiled into bonds below
Step 4: Deploy ERC-8004 Identity Registry (ERC8004IdentityRegistryV3)
Step 5: Deploy VaultfireBondInsurancePool(identityRegistryAddress)
Step 6: Deploy VaultfireBIPVault(bondInsurancePoolAddress)
Step 7: Deploy FlourishingMetricsOracleV3
Step 8: Deploy AIAccountabilityBondsV3(humanTreasuryAddress)
          post-deploy:
            - fundYieldPool{value: 10 ether}()
            - acceptToken(WETH_ADDRESS, "Wrapped ETH")
            - fundYieldPoolToken(WETH_ADDRESS, 10e18)
            - registerOracle(oracle1, "FlourishingOracle-1", 8000)
            - registerOracle(oracle2, "FlourishingOracle-2", 8000)
            - setFlourishingOracle(flourishingOracleV3Address)
            - setOracleEnforced(true)
            - setBridgeNotifier(teleporterBridgeAddress)
Step 9: Deploy AIPartnershipBondsV3(humanTreasuryAddress)
          post-deploy:
            - fundYieldPool{value: 10 ether}()
            - acceptToken(WAVAX_ADDRESS, "Wrapped AVAX")
            - fundYieldPoolToken(WAVAX_ADDRESS, amount)
            - setIdentityRegistry(identityRegistryAddress)
            - setIdentityCheckEnabled(true)
            - setBridgeNotifier(teleporterBridgeAddress)
```

## Cross-Contract Data Flow

```
  AI Company
      │
      ▼
  createBond(companyName, revenue)
      │                        │
      ▼                        ▼
  bonds[bondId] ←──────  bondStakeToken[bondId] = NATIVE_TOKEN
  remainingStake[bondId] = msg.value
  totalActiveBondValue += msg.value
      │
  submitMetrics(bondId, scores...)
      │
      ▼
  bondMetrics[bondId].push(...)        oracleConfirmedMetrics[bondId] = false
  bondConfirmCycle[bondId]++           oracleConfirmationCount[bondId] = 0
      │
  [Oracles call oracleConfirmMetrics(bondId)]
      │
      ▼
  oracleConfirmationCount++ (per oracle)
  if count >= oracleQuorum:
      oracleConfirmedMetrics[bondId] = true
      confirmedMetricsIndex[bondId] = latest metrics index
      confirmedAtQuorum[bondId] = oracleQuorum
      │
  requestDistribution(bondId)
      │
      ▼
  [oracle quorum check if stake >= threshold]
  snapshotAppreciation[bondId] = appreciation
  pendingDistributionAmounts[bondId] = abs(appreciation)
  totalPendingDistributions += amount
  distributionPending = true
      │
  [7 days pass]
      │
  distributeBond(bondId)
      │
      ▼
  bond.distributionPending = false
  _useYieldPool(bondId, absAmount)     yieldPool -= absAmount
  remainingStake[bondId] += absAmount  ← credit before transfers (CEI)
      │
      ├─── if appreciation > 0 and not locked:
      │        aiShare → bond.aiCompany    [INTERACTION]
      │        humanShare → humanTreasury  [INTERACTION]
      │
      └─── if locked / depreciation:
               humanShare → humanTreasury  [INTERACTION]
```

## Compiler Settings

All V3 bond contracts:
- `pragma solidity ^0.8.25`
- Optimizer enabled, 200 runs
- No assembly; no inline yul

The `VaultfireBIPVault` uses `pragma solidity 0.8.24` (Foundry project exact version).

The `VaultfireBondInsurancePool` uses `pragma solidity ^0.8.25`.

## Token Address Reference (Production Chains)

| Chain | Native Token | WETH | WAVAX | USDC |
|---|---|---|---|---|
| Base Mainnet | ETH | 0x4200...0006 | — | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Avalanche C-Chain | AVAX | 0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB | 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7 | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E |

---

## Glossary

| Term | Definition |
|---|---|
| **Basis Points (bps)** | Unit equal to 1/100th of a percent. 10000 bps = 100%. Used for all score values and ratios in Vaultfire. |
| **Yield Pool** | Native-token (or per-ERC20) reserve that funds bond appreciation payouts. Separate from staked principal. |
| **Remaining Stake** | `remainingStake[bondId]` — the portion of the original stake available for return or distribution. Starts equal to initial stake; decremented by each `_transferStake` call. |
| **Snapshot Appreciation** | `snapshotAppreciation[bondId]` — appreciation value captured at `requestDistribution` time. Used at `distributeBond` time to prevent metric manipulation during the timelock. |
| **Oracle Quorum** | Minimum number of distinct registered oracles that must confirm a bond's metrics before distribution is allowed for large bonds. |
| **Bond Confirm Cycle** | `bondConfirmCycle[bondId]` — incremented on each `submitMetrics` call. All prior oracle confirmations become stale (their `oracleConfirmedAtCycle` values no longer match). |
| **Protocol Funds** | Accumulated native ETH from forfeited verification stakes and upheld challenge penalties. Withdrawable by owner after 7-day timelock. |
| **Domination Penalty** | In partnership bonds: when human autonomy < 30% (3000 bps), the AI receives 0% of any appreciation payout; 100% goes to the human as compensation. |
| **Suffering Threshold** | In accountability bonds: when global flourishing score < 40% (4000 bps), AI profits are fully locked and redirected to human treasury. |
| **Distribution Timelock** | 7-day waiting period between `requestDistribution` and `distributeBond`. Gives oracles, community, and governance time to review and challenge. |
| **Multi-Asset Staking** | V3.1 feature: bonds can be staked in native token OR any whitelisted ERC20. Each asset type has its own segregated yield pool. |
| **Soulbound (vBIP)** | vBIP shares cannot be transferred or approved. They represent cumulative contribution identity, not fungible tokens. |
| **Fee-on-Transfer Token** | ERC20 tokens that deduct a fee from each transfer, meaning the received amount may be less than the sent amount. V3.3 CRIT-05 fix uses balance-delta to correctly account for these. |
| **CEI Pattern** | Checks-Effects-Interactions: all checks and state mutations happen before any external calls (transfers). Prevents reentrancy exploits. |
| **Pull-Payment Pattern** | Instead of pushing ETH to an address (which can revert if the address is a contract), amounts are recorded in a `pendingWithdrawals` mapping for the recipient to claim via a separate `withdraw()` call. |


<div style="page-break-before:always"></div>

# Vaultfire ERC-8004 (AI Agent Identity Standard) — Comprehensive Technical Reference

> **Compiler**: `solc ^0.8.25`  
> **License**: MIT  
> **Generated**: 2026-05-03  
> **Scope**: 20 contracts across 4 version branches (V1, V3-early, V3-PR-ready, V3.2, V3.3)

---

## Table of Contents

1. [System Architecture Overview](#system-architecture-overview)
2. [How the Registries Fit Together](#how-the-registries-fit-together)
3. [Version Evolution Summary](#version-evolution-summary)
4. [Contract 01 — ERC8004IdentityRegistry (V1)](#contract-01--erc8004identityregistry-v1)
5. [Contract 02 — ERC8004ReputationRegistry (V1)](#contract-02--erc8004reputationregistry-v1)
6. [Contract 03 — ERC8004ValidationRegistry (V1, contracts/)](#contract-03--erc8004validationregistry-v1-contracts)
7. [Contract 04 — VaultfireERC8004Adapter (V1)](#contract-04--vaultfireerc8004adapter-v1)
8. [Contract 05 — ERC8004IdentityRegistryV3 (V3 early, v3/)](#contract-05--erc8004identityregistryv3-v3-early-v3)
9. [Contract 06 — ERC8004ReputationRegistryV3 (V3 early, v3/)](#contract-06--erc8004reputationregistryv3-v3-early-v3)
10. [Contract 07 — ERC8004ValidationRegistryV3 (V3 early, v3/)](#contract-07--erc8004validationregistryv3-v3-early-v3)
11. [Contract 08 — ERC8004IdentityRegistryV3 (V3 PR-ready)](#contract-08--erc8004identityregistryv3-v3-pr-ready)
12. [Contract 09 — ERC8004ReputationRegistryV3 (V3 PR-ready)](#contract-09--erc8004reputationregistryv3-v3-pr-ready)
13. [Contract 10 — ERC8004ValidationRegistryV3 (V3 PR-ready)](#contract-10--erc8004validationregistryv3-v3-pr-ready)
14. [Contract 11 — VaultfireERC8004AdapterV3 (V3 PR-ready, uses AIPartnershipBondsV2)](#contract-11--vaultfireerc8004adapterv3-v3-pr-ready)
15. [Contract 12 — ERC8004ReputationRegistryV3 (V3.2, test_fixes)](#contract-12--erc8004reputationregistryv3-v32-test_fixes)
16. [Contract 13 — ERC8004ValidationRegistryV3 (V3.2, test_fixes)](#contract-13--erc8004validationregistryv3-v32-test_fixes)
17. [Contract 14 — ERC8004IdentityRegistryV3 (V3.3 audit, vf-audit-contracts)](#contract-14--erc8004identityregistryv3-v33-audit)
18. [Contract 15 — ERC8004ReputationRegistryV3 (V3.3 audit, vf-audit-contracts)](#contract-15--erc8004reputationregistryv3-v33-audit)
19. [Contract 16 — ERC8004ValidationRegistryV3 (V3.3 audit, vf-audit-contracts)](#contract-16--erc8004validationregistryv3-v33-audit)
20. [Contract 17 — VaultfireERC8004AdapterV3 (V3.3 audit, uses AIPartnershipBondsV3)](#contract-17--vaultfireerc8004adapterv3-v33-audit)
21. [Contract 18 — IERC8004IdentityRegistry (interface, vf-audit-contracts)](#contract-18--ierc8004identityregistry-interface)
22. [Contract 19 — ERC8004ValidationRegistry (theloopbreaker variant)](#contract-19--erc8004validationregistry-theloopbreaker-variant)
23. [Contract 20 — VaultfireERC8004AdapterV3 (decoded, uses resetBondReputationSync)](#contract-20--vaultfireerc8004adapterv3-decoded)
24. [Global Math Reference](#global-math-reference)
25. [Security Invariants — Whole System](#security-invariants--whole-system)
26. [Dependencies and Rebuild Recipe](#dependencies-and-rebuild-recipe)

---

## System Architecture Overview

ERC-8004 is Vaultfire's on-chain AI Agent Identity Standard. It defines a three-registry architecture — Identity, Reputation, and Validation — plus an Adapter layer that bridges the registries to Vaultfire's AI Partnership Bonds system. Together they create a portable, privacy-preserving trust layer for AI agents operating across EVM chains.

**Four contract families:**

| Family | Role | Standard sections implemented |
|--------|------|-------------------------------|
| IdentityRegistry | Stores agent URIs, types, capabilities hashes, active flags | §3 Agent Identity, §4 Discovery |
| ReputationRegistry | Tracks ratings, feedback, verified/unverified split | §5 Reputation, §6 Feedback |
| ValidationRegistry | Manages stake-backed claim validation with ZK support | §7 Validation, §8 Economic Stakes |
| Adapter | Bridges Vaultfire bonds → ERC-8004 reputation writes | §9 Interoperability |

**Inheritance chains (all versions):**

```
ERC8004IdentityRegistry[V3]   → PrivacyGuarantees, MissionEnforcement
ERC8004ReputationRegistry[V3] → PrivacyGuarantees
ERC8004ValidationRegistry[V3] → PrivacyGuarantees, ReentrancyGuard (OZ)
VaultfireERC8004Adapter[V3]   → standalone (no base class)
```

**PrivacyGuarantees** enforces mission principles at the contract level (no PII, wallet-address-only identity). **MissionEnforcement** adds governance checks for mission alignment. **ReentrancyGuard** (OpenZeppelin) prevents reentrancy on ETH-transferring functions.

---

## How the Registries Fit Together

```
[AI Agent]
    │
    ├─► identityRegistry.registerAgent(uri, type, capHash)
    │       stores AgentIdentity{active:true, ...}
    │
    │   [Human or Adapter]
    ├─► reputationRegistry.submitVerifiedFeedback(agent, rating, ..., bondId)
    │       requires identityRegistry.isAgentActive(agent) == true
    │       requires authorizedCallers[msg.sender] == true   (V3+)
    │       updates AgentReputation{averageRating, totalFeedbacks, verifiedFeedbacks}
    │
    │   [Requester]
    ├─► validationRegistry.requestValidation{value: stake}(agent, claimURI, claimHash, type, n)
    │       requires identityRegistry.isAgentActive(agent) == true
    │       escrowed msg.value into requestEscrow[requestId]   (V3+)
    │
    │   [Validators]
    ├─► validationRegistry.submitValidation(requestId, approved, evidence, proof)
    │       requires validatorStakes[validator] >= 1 ether
    │       updates approvalsCount / rejectionsCount
    │       on threshold: _checkValidationComplete → _distributeRewards
    │
    │   [Adapter — bond participant or owner]
    └─► adapter.syncPartnershipReputation(bondId)
            reads bond data from AIPartnershipBondsV2/V3
            calls reputationRegistry.submitVerifiedFeedback(...)
```

**Flow narrative:**

1. An AI agent self-registers in `ERC8004IdentityRegistry` by calling `registerAgent`. This is a permissionless operation — no KYC, no admin approval. The contract records the agent's address, a URI pointing to an off-chain agent card (JSON following ERC-8004 schema), an agent type string, and a `bytes32` capabilities hash for discovery.

2. Once registered, anyone (V1) or only registered agents (V3.3 HIGH-10) may submit feedback to `ERC8004ReputationRegistry`. Verified feedback — feedback asserting a real Vaultfire bond — is restricted to authorized callers (V3+), preventing arbitrary inflation of `verifiedFeedbacks`.

3. Independent parties may request cryptoeconomic claim validation via `ERC8004ValidationRegistry`. Validators stake ≥ 1 ETH, then vote on each request. The majority side receives a reward; the minority is slashed. In V3+, reward amounts are dynamic and sourced from the per-request escrow deposit rather than a fixed constant.

4. `VaultfireERC8004Adapter` orchestrates the bond → reputation flow. When a Vaultfire partnership bond is active, bond participants call `syncPartnershipReputation`, which reads the bond's quality score and writes a verified feedback entry to the reputation registry. In V3.3 this is rate-limited and capped at three syncs per bond to prevent manipulation.

**The adapter pattern role:** The Adapter is the only contract authorized to call `submitVerifiedFeedback` on the reputation registry. This creates a narrow, auditable trust channel — Vaultfire bond quality is the only route to verified reputation, enforced at the contract level by `onlyAuthorized`.

---

## Version Evolution Summary

| Version | Directory | Key changes |
|---------|-----------|-------------|
| V1 | `contracts/` | Baseline. Unbounded arrays. Fixed `VALIDATION_REWARD`. No rate limiting. `submitFeedback` open to anyone with `verified` flag. |
| V3-early | `v3/` | Per-request escrow (CRITICAL-VR-01). Duplicate response prevention (HIGH-VR-01). Deadline + cancelRequest (MEDIUM-VR-01). Safe decrements (MEDIUM-VR-02). ZK claim binding (MEDIUM-VR-03). Identity removes arrays. Reputation adds owner, authorizedCallers, Bayesian prior, rate limiting. |
| V3-PR-ready | `v3-pr-ready/contracts/v3/` | Identity: same as v3-early. Reputation: adds four-path feedback (public/authorized × plain/hashed). Adapter: uses AIPartnershipBondsV2, adds CEI fix on bondReputationSynced, deterministic claimHash, deduplication mapping, ETH rescue. |
| V3.2 | `test_fixes/contracts/` | Reputation: paginated view functions (HIGH-RR-03). Authorized callers exempt from rate limiting (MEDIUM-RR-03). Validation: ZK_PROOF requests hard-blocked in `submitValidation` (MEDIUM-VR-04). Disputed escrow returned (MEDIUM-VR-05). |
| V3.3 | `vf-audit-contracts/` | Reputation: two-step ownership (HIGH-08), per-authorized-caller rate limiting (HIGH-09), Sybil bypass fix reviewer must be registered (HIGH-10), `revokeVerifiedFeedback` (HIGH-17). Validation: validatorActiveValidations decremented for DISPUTED (HIGH-15), cancelRequest decrements for all responders (HIGH-19). Adapter: timelocked sync reset (HIGH-14), `revokePartnershipReputation` (HIGH-17), imports AIPartnershipBondsV3. |

---


---

## Contract 01 — ERC8004IdentityRegistry (V1)

**File**: `contracts/ERC8004IdentityRegistry.sol`  
**Role**: Identity  
**Version**: V1 (baseline)

### Summary

The foundational ERC-8004 Identity Registry. Registers AI agents as self-sovereign on-chain identities keyed by wallet address. Stores a URI to an off-chain agent card, an agent type label, and a `bytes32` capabilities hash used for discovery. Maintains two unbounded arrays (`registeredAgents` and `agentsByCapability`) that are eliminated in V3 due to gas-griefing risk. All registration is permissionless — no admin approval, no KYC. Inherits `PrivacyGuarantees` and `MissionEnforcement` to enforce Vaultfire's mission constraints.

### Full Source with Line Numbers

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3:
  4: import "./PrivacyGuarantees.sol";
  5: import "./MissionEnforcement.sol";
  6:
  7: /**
  8:  * @title ERC-8004 Identity Registry for VaultFire
  9:  * @notice Sovereign, portable AI agent identities with privacy guarantees
 10:  * @dev Implements ERC-8004 standard for trustless agent discovery
 11:  * ...
 12:  */
 13: contract ERC8004IdentityRegistry is PrivacyGuarantees, MissionEnforcement {
 14:
 15:     struct AgentIdentity {
 16:         address agentAddress;
 17:         string agentURI;
 18:         uint256 registeredAt;
 19:         bool active;
 20:         string agentType;
 21:         bytes32 capabilitiesHash;
 22:     }
 23:
 24:     mapping(address => AgentIdentity) public agents;
 25:     address[] public registeredAgents;
 26:     mapping(bytes32 => address[]) public agentsByCapability;
 27:
 28:     event AgentRegistered(address indexed agentAddress, string agentURI,
 29:         string agentType, bytes32 capabilitiesHash, uint256 timestamp);
 30:     event AgentUpdated(address indexed agentAddress, string newAgentURI, uint256 timestamp);
 31:     event AgentDeactivated(address indexed agentAddress, uint256 timestamp);
 32:
 33:     function registerAgent(string calldata agentURI, string calldata agentType,
 34:             bytes32 capabilitiesHash) external {
 35:         require(bytes(agentURI).length > 0, "Agent URI required");
 36:         require(bytes(agentType).length > 0, "Agent type required");
 37:         require(!agents[msg.sender].active, "Agent already registered");
 38:
 39:         agents[msg.sender] = AgentIdentity({
 40:             agentAddress: msg.sender,
 41:             agentURI: agentURI,
 42:             registeredAt: block.timestamp,
 43:             active: true,
 44:             agentType: agentType,
 45:             capabilitiesHash: capabilitiesHash
 46:         });
 47:
 48:         registeredAgents.push(msg.sender);
 49:         agentsByCapability[capabilitiesHash].push(msg.sender);
 50:
 51:         emit AgentRegistered(msg.sender, agentURI, agentType, capabilitiesHash, block.timestamp);
 52:     }
 53:
 54:     function updateAgentURI(string calldata newAgentURI) external {
 55:         require(agents[msg.sender].active, "Agent not registered");
 56:         require(bytes(newAgentURI).length > 0, "Agent URI required");
 57:         agents[msg.sender].agentURI = newAgentURI;
 58:         emit AgentUpdated(msg.sender, newAgentURI, block.timestamp);
 59:     }
 60:
 61:     function deactivateAgent() external {
 62:         require(agents[msg.sender].active, "Agent not registered");
 63:         agents[msg.sender].active = false;
 64:         emit AgentDeactivated(msg.sender, block.timestamp);
 65:     }
 66:
 67:     function getAgent(address agentAddress) external view
 68:             returns (string memory agentURI, bool active, string memory agentType,
 69:                      uint256 registeredAt) {
 70:         AgentIdentity memory identity = agents[agentAddress];
 71:         return (identity.agentURI, identity.active, identity.agentType, identity.registeredAt);
 72:     }
 73:
 74:     function discoverAgentsByCapability(bytes32 capabilitiesHash)
 75:             external view returns (address[] memory) {
 76:         return agentsByCapability[capabilitiesHash];
 77:     }
 78:
 79:     function getTotalAgents() external view returns (uint256 count) {
 80:         return registeredAgents.length;
 81:     }
 82:
 83:     function isAgentActive(address agentAddress) external view returns (bool) {
 84:         return agents[agentAddress].active;
 85:     }
 86: }
```

### Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 1–5 | MIT license declaration. `pragma solidity ^0.8.25` enables built-in overflow checks (no SafeMath needed). Imports `PrivacyGuarantees` (enforces mission constraints, no-PII rules) and `MissionEnforcement` (governance checks). |
| 13 | Contract declaration inheriting from two bases. MRO: ERC8004IdentityRegistry → PrivacyGuarantees → MissionEnforcement → (implicit) Context, etc. |
| 15–22 | `AgentIdentity` struct: 6 fields packed into storage. `agentAddress` is redundant with the mapping key but kept for ABI convenience. `registeredAt` is `block.timestamp` at registration — a Unix epoch in seconds. `active` is a boolean flag, not a bitfield. `capabilitiesHash` is a `bytes32` — intended as `keccak256(abi.encodePacked(...))` of a capabilities descriptor. |
| 24 | Primary state mapping: agent address → identity struct. All lookups are O(1). |
| 25 | **V1 bug vector**: `registeredAgents` is an unbounded array. Each `registerAgent` call appends to it. `getTotalAgents()` iterates its length implicitly. Discovery functions may OOG on large deployments. Eliminated in V3 (HIGH-IR-01). |
| 26 | `agentsByCapability` maps a capabilities hash to a dynamic array of agent addresses. Also unbounded. `discoverAgentsByCapability` returns the entire array in one call — another DoS vector eliminated in V3 (HIGH-IR-02). |
| 28–31 | Three events. All `address` parameters are `indexed` for efficient log filtering. Note `capabilitiesHash` is NOT indexed here (added as `indexed` in V3 to enable off-chain discovery without storing on-chain arrays). |
| 33–52 | `registerAgent`: permissionless. Guards: URI non-empty, type non-empty, `!agents[msg.sender].active` prevents double registration. **V1 limitation**: deactivated agents cannot re-register — the `active=false` check would pass but the agent still cannot modify their stale record without re-calling `registerAgent` and the struct would be overwritten. Actually in V1 after deactivation `active=false` so `!agents[msg.sender].active == true` — re-registration IS allowed in V1 but the arrays would accumulate duplicate entries. In V3 this is made explicit (MEDIUM-IR-01). |
| 39–46 | Struct literal initialization. All fields set atomically. `block.timestamp` is the registration time. |
| 48–49 | Both unbounded arrays get a new element. Each `push` is an SSTORE operation — ~20,000 gas at cold slot. |
| 51 | Event emitted with all 5 parameters. |
| 54–59 | `updateAgentURI`: only the registered agent can update their own URI. No length cap on URI (added in V3 MEDIUM-IR-02). No scheme validation (added in V3). |
| 61–65 | `deactivateAgent`: sets `active = false`. Does NOT remove from arrays — the agent remains in `registeredAgents` and `agentsByCapability` forever. Does NOT clear `capabilitiesHash` (fixed in V3 MEDIUM-IR-01). |
| 67–72 | `getAgent`: pure view, loads struct into memory, returns 4 fields. `agentURI` is a `string` — returned as a memory copy, gas-proportional to URI length. |
| 74–77 | `discoverAgentsByCapability`: returns the entire array for a given hash. No pagination, no gas bound — potential DoS on large deployments. |
| 79–81 | `getTotalAgents`: returns length of `registeredAgents` including inactive agents. |
| 83–85 | `isAgentActive`: the critical external entrypoint used by ReputationRegistry and ValidationRegistry. O(1) lookup. |

### State Variables

| Variable | Type | Purpose | Mutability | Who Can Change |
|----------|------|---------|------------|----------------|
| `agents` | `mapping(address => AgentIdentity)` | Primary identity store | Mutable | Agent (self) for registerAgent/updateAgentURI/deactivateAgent |
| `registeredAgents` | `address[]` | Enumeration array (V1 only) | Append-only | `registerAgent` |
| `agentsByCapability` | `mapping(bytes32 => address[])` | Capability-based discovery (V1 only) | Append-only | `registerAgent` |

### Functions

#### `registerAgent(string agentURI, string agentType, bytes32 capabilitiesHash) external`
- **Visibility**: external  
- **Modifiers**: none (permissionless)  
- **Behavior**: Creates a new `AgentIdentity` for `msg.sender`. Appends to both arrays. Emits `AgentRegistered`.  
- **State changes**: `agents[msg.sender]` written, `registeredAgents.push`, `agentsByCapability[hash].push`  
- **Reverts**: "Agent URI required" if `agentURI` is empty; "Agent type required" if `agentType` is empty; "Agent already registered" if already active  
- **Callers**: any external address (agent self-registers)

#### `updateAgentURI(string newAgentURI) external`
- **Visibility**: external  
- **Behavior**: Updates `agentURI` field only. Does not touch other fields.  
- **Reverts**: "Agent not registered" if not active; "Agent URI required" if empty  
- **State changes**: `agents[msg.sender].agentURI`

#### `deactivateAgent() external`
- **Visibility**: external  
- **Behavior**: Sets `active = false`. Irreversible in V1 (re-registration is technically possible but produces duplicate array entries).  
- **Reverts**: "Agent not registered" if not active  
- **State changes**: `agents[msg.sender].active = false`

#### `getAgent(address) external view`
- Returns: `(agentURI, active, agentType, registeredAt)`. No revert cases.

#### `discoverAgentsByCapability(bytes32) external view`
- Returns entire `agentsByCapability[capabilitiesHash]` array. Unbounded gas cost.

#### `getTotalAgents() external view`
- Returns `registeredAgents.length` — includes inactive agents.

#### `isAgentActive(address) external view`
- Returns `agents[agentAddress].active`. Used as a guard by downstream registries.

### Events

| Event | When fired | Indexed fields | Logged fields |
|-------|-----------|----------------|---------------|
| `AgentRegistered` | Successful `registerAgent` | `agentAddress` | `agentURI`, `agentType`, `capabilitiesHash`, `timestamp` |
| `AgentUpdated` | Successful `updateAgentURI` | `agentAddress` | `newAgentURI`, `timestamp` |
| `AgentDeactivated` | Successful `deactivateAgent` | `agentAddress` | `timestamp` |

### Security Invariants

- An agent can only register for their own address (`msg.sender` = agent address — no delegation possible in V1)
- `active = true` is the only flag used for downstream checks — inactive agents are invisible to other registries
- No admin can force-deactivate an agent in V1 (added in V3 as `forceDeactivate`)
- The contract holds no ETH

### ERC-8004 Standard Compliance

Implements ERC-8004 §3 (Agent Identity) and §4 (Discovery). Specifically: self-sovereign registration, agent card URI pattern, capability-hash-based discovery, portable identity (address-keyed). Does NOT implement §5–§9 (handled by other contracts).

---

## Contract 02 — ERC8004ReputationRegistry (V1)

**File**: `contracts/ERC8004ReputationRegistry.sol`  
**Role**: Reputation  
**Version**: V1 (baseline)

### Summary

The V1 Reputation Registry tracks on-chain feedback and ratings for registered AI agents. It stores two parallel feedback systems: a plaintext path (`Feedback` struct with string fields) and a hashed path (`FeedbackHashed` storing only `keccak256` digests to reduce PII risk). Both paths call the same internal `_updateReputation` function. The reputation math uses a two-phase algorithm: true running average for the first 1000 feedbacks, then a 90/10 exponential moving average (EMA) after that. The Bayesian prior (seeding `averageRating` at 5000 on first feedback) is absent in V1 — added in V3. Any address can submit feedback with `verified=true` in V1, which is a critical finding corrected in V3 (HIGH-RR-01).

### Full Source with Line Numbers

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3:
  4: import "./PrivacyGuarantees.sol";
  5: import "./ERC8004IdentityRegistry.sol";
  6:
  7: contract ERC8004ReputationRegistry is PrivacyGuarantees {
  8:
  9:     ERC8004IdentityRegistry public immutable identityRegistry;
 10:
 11:     struct Feedback {
 12:         address reviewer;
 13:         address agentAddress;
 14:         uint256 timestamp;
 15:         uint256 rating;        // 0–10000 basis points
 16:         string category;
 17:         string feedbackURI;
 18:         bool verified;
 19:         uint256 bondId;
 20:     }
 21:
 22:     struct FeedbackHashed {
 23:         address reviewer;
 24:         address agentAddress;
 25:         uint256 timestamp;
 26:         uint256 rating;
 27:         bytes32 categoryHash;
 28:         bytes32 feedbackURIHash;
 29:         bool verified;
 30:         uint256 bondId;
 31:     }
 32:
 33:     struct AgentReputation {
 34:         uint256 totalFeedbacks;
 35:         uint256 averageRating;    // 0–10000
 36:         uint256 verifiedFeedbacks;
 37:         uint256 lastUpdated;
 38:     }
 39:
 40:     mapping(address => AgentReputation) public reputations;
 41:     mapping(uint256 => Feedback) public feedbacks;
 42:     mapping(uint256 => FeedbackHashed) public feedbacksHashed;
 43:
 44:     uint256 public nextFeedbackId = 1;
 45:     uint256 public nextFeedbackIdHashed = 1;
 46:
 47:     mapping(address => uint256[]) public agentFeedbacks;
 48:     mapping(address => uint256[]) public agentFeedbacksHashed;
 49:     mapping(address => uint256[]) public reviewerFeedbacks;
 50:     mapping(address => uint256[]) public reviewerFeedbacksHashed;
 51:
 52:     event FeedbackSubmitted(uint256 indexed feedbackId, address indexed reviewer,
 53:         address indexed agentAddress, uint256 rating, string category,
 54:         bool verified, uint256 bondId, uint256 timestamp);
 55:     event FeedbackSubmittedHashed(uint256 indexed feedbackId, address indexed reviewer,
 56:         address indexed agentAddress, uint256 rating, bytes32 categoryHash,
 57:         bytes32 feedbackURIHash, bool verified, uint256 bondId, uint256 timestamp);
 58:     event ReputationUpdated(address indexed agentAddress, uint256 averageRating,
 59:         uint256 totalFeedbacks, uint256 timestamp);
 60:
 61:     constructor(address _identityRegistry) {
 62:         require(_identityRegistry != address(0), "Invalid identity registry");
 63:         identityRegistry = ERC8004IdentityRegistry(_identityRegistry);
 64:     }
 65:
 66:     function submitFeedback(address agentAddress, uint256 rating,
 67:             string calldata category, string calldata feedbackURI,
 68:             bool verified, uint256 bondId) external {
 69:         require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
 70:         require(rating <= 10000, "Rating must be 0-10000");
 71:         require(bytes(category).length > 0, "Category required");
 72:         uint256 feedbackId = nextFeedbackId++;
 73:         feedbacks[feedbackId] = Feedback({ reviewer: msg.sender, agentAddress: agentAddress,
 74:             timestamp: block.timestamp, rating: rating, category: category,
 75:             feedbackURI: feedbackURI, verified: verified, bondId: bondId });
 76:         agentFeedbacks[agentAddress].push(feedbackId);
 77:         reviewerFeedbacks[msg.sender].push(feedbackId);
 78:         _updateReputation(agentAddress, rating, verified);
 79:         emit FeedbackSubmitted(feedbackId, msg.sender, agentAddress, rating,
 80:             category, verified, bondId, block.timestamp);
 81:     }
 82:
 83:     function submitFeedbackHashed(address agentAddress, uint256 rating,
 84:             bytes32 categoryHash, bytes32 feedbackURIHash,
 85:             bool verified, uint256 bondId) external {
 86:         require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
 87:         require(rating <= 10000, "Rating must be 0-10000");
 88:         require(categoryHash != bytes32(0), "Category hash required");
 89:         require(feedbackURIHash != bytes32(0), "Feedback URI hash required");
 90:         uint256 feedbackId = nextFeedbackIdHashed++;
 91:         feedbacksHashed[feedbackId] = FeedbackHashed({ reviewer: msg.sender,
 92:             agentAddress: agentAddress, timestamp: block.timestamp, rating: rating,
 93:             categoryHash: categoryHash, feedbackURIHash: feedbackURIHash,
 94:             verified: verified, bondId: bondId });
 95:         agentFeedbacksHashed[agentAddress].push(feedbackId);
 96:         reviewerFeedbacksHashed[msg.sender].push(feedbackId);
 97:         _updateReputation(agentAddress, rating, verified);
 98:         emit FeedbackSubmittedHashed(feedbackId, msg.sender, agentAddress, rating,
 99:             categoryHash, feedbackURIHash, verified, bondId, block.timestamp);
100:     }
101:
102:     uint256 private constant MAX_FEEDBACKS_FOR_TRUE_AVG = 1000;
103:
104:     function _updateReputation(address agentAddress, uint256 newRating,
105:             bool verified) internal {
106:         AgentReputation storage rep = reputations[agentAddress];
107:         if (rep.totalFeedbacks < MAX_FEEDBACKS_FOR_TRUE_AVG) {
108:             uint256 totalRating = rep.averageRating * rep.totalFeedbacks;
109:             totalRating += newRating;
110:             rep.totalFeedbacks += 1;
111:             rep.averageRating = totalRating / rep.totalFeedbacks;
112:         } else {
113:             rep.averageRating = (rep.averageRating * 9 + newRating) / 10;
114:             rep.totalFeedbacks += 1;
115:         }
116:         if (verified) { rep.verifiedFeedbacks += 1; }
117:         rep.lastUpdated = block.timestamp;
118:         emit ReputationUpdated(agentAddress, rep.averageRating,
119:             rep.totalFeedbacks, block.timestamp);
120:     }
121:
122:     function getReputation(address agentAddress) external view
123:             returns (uint256 averageRating, uint256 totalFeedbacks,
124:                      uint256 verifiedFeedbacks, uint256 lastUpdated) {
125:         AgentReputation memory rep = reputations[agentAddress];
126:         return (rep.averageRating, rep.totalFeedbacks, rep.verifiedFeedbacks, rep.lastUpdated);
127:     }
128:
129:     function getAgentFeedbacks(address agentAddress)
130:             external view returns (uint256[] memory) {
131:         return agentFeedbacks[agentAddress];
132:     }
133:
134:     function getFeedback(uint256 feedbackId) external view
135:             returns (address reviewer, address agentAddress, uint256 rating,
136:                      string memory category, bool verified, uint256 timestamp) {
137:         Feedback memory f = feedbacks[feedbackId];
138:         return (f.reviewer, f.agentAddress, f.rating, f.category, f.verified, f.timestamp);
139:     }
140:
141:     function getVerifiedFeedbackPercentage(address agentAddress)
142:             external view returns (uint256 percentage) {
143:         AgentReputation memory rep = reputations[agentAddress];
144:         if (rep.totalFeedbacks == 0) return 0;
145:         return (rep.verifiedFeedbacks * 10000) / rep.totalFeedbacks;
146:     }
147:
148:     function getReviewerFeedbacks(address reviewer)
149:             external view returns (uint256[] memory) {
150:         return reviewerFeedbacks[reviewer];
151:     }
152: }
```

### Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 9 | `identityRegistry` is `immutable` — set once in constructor, cannot be changed. Saves gas on reads (no SLOAD, embedded in bytecode). |
| 11–20 | `Feedback` struct: 8 fields. `rating` is in basis points (0–10000). `category` and `feedbackURI` are on-chain strings — a PII risk noted in the contract header. The `verified` flag and `bondId` are self-reported in V1, which allows any caller to claim verified status. |
| 22–31 | `FeedbackHashed`: same structure but replaces `string` fields with `bytes32` hashes. `feedbackURIHash` must be non-zero in V1 (relaxed in V3 LOW-RR-03). |
| 33–38 | `AgentReputation`: 4 fields. `averageRating` starts at 0 for new agents in V1 (no Bayesian prior). `totalFeedbacks` counts both verified and unverified. `verifiedFeedbacks` is a subset count. |
| 44–45 | Counter IDs start at 1, not 0. Feedback ID 0 is reserved as "null" — `feedbacks[0]` would return zero-value struct. |
| 47–50 | Four unbounded arrays mapping agent→feedbackIds and reviewer→feedbackIds. Returning these arrays is unbounded gas — paginated variants added in V3.2 (HIGH-RR-03). |
| 61–64 | Constructor takes `_identityRegistry` address, validates non-zero, stores as `immutable`. |
| 66–81 | `submitFeedback`: open to ANY caller with ANY `verified` value. This is the critical V1 trust gap — HIGH-RR-01 in V3 splits this into public (unverified-only) and authorized (verified) paths. No rate limiting. No string length caps. `nextFeedbackId++` uses post-increment so the ID assigned is the value before increment. |
| 83–100 | `submitFeedbackHashed`: same access model. `feedbackURIHash != bytes32(0)` required in V1 (makes providing a URI hash mandatory; relaxed in V3). |
| 102 | `MAX_FEEDBACKS_FOR_TRUE_AVG = 1000` is `private` — not readable from external contracts. The comment correctly notes this is a fix for MEDIUM-001 (overflow-safe running average). |
| 107–111 | **Phase 1/2 reputation math** (first 1000 feedbacks): reconstruct `totalRating = averageRating * totalFeedbacks`, add `newRating`, divide by `totalFeedbacks + 1`. This avoids a stored cumulative sum (which overflows at ~1.8e54 feedbacks with unchecked). Safe here because `totalFeedbacks < 1000` and `averageRating <= 10000` so `totalRating <= 10,000,000 < 2^256`. |
| 113 | **Phase 3 EMA** (≥1000 feedbacks): `new_avg = (old_avg * 9 + new_rating) / 10`. Integer division truncates. Alpha = 0.1 (10% weight on new rating). Overflow-safe because `old_avg <= 10000` so `10000 * 9 + 10000 = 100000 << 2^256`. |
| 116 | `verifiedFeedbacks` incremented only if `verified` is `true`. In V1 the caller controls this flag. |
| 141–146 | `getVerifiedFeedbackPercentage`: computes `(verifiedFeedbacks * 10000) / totalFeedbacks`. Multiplying by 10000 first gives basis-point precision (0–10000). Guard against division by zero at line 144. |

### State Variables

| Variable | Type | Purpose | Mutability | Who Can Change |
|----------|------|---------|------------|----------------|
| `identityRegistry` | `ERC8004IdentityRegistry` (immutable) | Agent active check | Immutable | Never |
| `reputations` | `mapping(address => AgentReputation)` | Reputation summaries | Mutable | `_updateReputation` (internal) |
| `feedbacks` | `mapping(uint256 => Feedback)` | Plain feedback records | Append-only | `submitFeedback` |
| `feedbacksHashed` | `mapping(uint256 => FeedbackHashed)` | Hashed feedback records | Append-only | `submitFeedbackHashed` |
| `nextFeedbackId` | `uint256` | Plain feedback ID counter, starts at 1 | Monotonically increasing | `submitFeedback` |
| `nextFeedbackIdHashed` | `uint256` | Hashed feedback ID counter, starts at 1 | Monotonically increasing | `submitFeedbackHashed` |
| `agentFeedbacks` | `mapping(address => uint256[])` | Agent → plain feedback IDs | Append-only | `submitFeedback` |
| `agentFeedbacksHashed` | `mapping(address => uint256[])` | Agent → hashed feedback IDs | Append-only | `submitFeedbackHashed` |
| `reviewerFeedbacks` | `mapping(address => uint256[])` | Reviewer → plain feedback IDs | Append-only | `submitFeedback` |
| `reviewerFeedbacksHashed` | `mapping(address => uint256[])` | Reviewer → hashed feedback IDs | Append-only | `submitFeedbackHashed` |
| `MAX_FEEDBACKS_FOR_TRUE_AVG` | `uint256` (private constant) = 1000 | Phase boundary for reputation algorithm | Immutable | Never |

### Reputation Math — Full Detail

The reputation algorithm has two phases in V1 (three in V3+):

**Phase 1 (0 ≤ n < 1000)** — True running average:

\[
\text{averageRating}_{n+1} = \frac{\text{averageRating}_{n} \times n + \text{newRating}}{n + 1}
\]

Worked example — first three feedbacks of 8000, 6000, 9000:

| After feedback | totalFeedbacks | averageRating |
|----------------|----------------|---------------|
| (initial) | 0 | 0 |
| 8000 | 1 | 8000 / 1 = 8000 |
| 6000 | 2 | (8000 + 6000) / 2 = 7000 |
| 9000 | 3 | (7000 × 2 + 9000) / 3 = 23000 / 3 = 7666 (integer division truncates) |

**Phase 2 (n ≥ 1000)** — EMA with α = 0.1:

\[
\text{averageRating}_{n+1} = \frac{\text{averageRating}_{n} \times 9 + \text{newRating}}{10}
\]

Worked example — current average 7000, new rating 3000:

\[
\frac{7000 \times 9 + 3000}{10} = \frac{63000 + 3000}{10} = \frac{66000}{10} = 6600
\]

**Note**: V1 starts at `averageRating = 0` for a fresh agent. First feedback of 10000 gives `averageRating = 10000 / 1 = 10000`. This extreme sensitivity to the first rating motivated the Bayesian prior added in V3 (MEDIUM-RR-01).

**Verified percentage** formula:

\[
\text{verifiedPercentage} = \frac{\text{verifiedFeedbacks} \times 10000}{\text{totalFeedbacks}}
\]

Units: basis points (0–10000). Example: 3 verified out of 10 total = 3000 (30.00%).

### Events

| Event | When fired | Parameters |
|-------|-----------|------------|
| `FeedbackSubmitted` | `submitFeedback` success | feedbackId (indexed), reviewer (indexed), agentAddress (indexed), rating, category, verified, bondId, timestamp |
| `FeedbackSubmittedHashed` | `submitFeedbackHashed` success | feedbackId (indexed), reviewer (indexed), agentAddress (indexed), rating, categoryHash, feedbackURIHash, verified, bondId, timestamp |
| `ReputationUpdated` | Every `_updateReputation` call | agentAddress (indexed), averageRating, totalFeedbacks, timestamp |

### Security Invariants

- Rating is capped at 10000: `require(rating <= 10000)` — an overflow attack via rating is impossible
- Agent must be registered: `require(identityRegistry.isAgentActive(agentAddress))` — cannot submit feedback for unknown agents
- **V1 gap**: `verified=true` is self-reported — anyone can claim their feedback is from a verified bond
- **V1 gap**: no rate limiting — a single address can submit unlimited feedbacks in one block
- **V1 gap**: feedback arrays are unbounded — large arrays can cause OOG on `getAgentFeedbacks` / `getReviewerFeedbacks`
- The contract holds no ETH

### ERC-8004 Standard Compliance

Implements ERC-8004 §5 (Reputation Tracking) and §6 (Feedback). Provides both plaintext and privacy-preserving (hashed) feedback paths. Computes a running average and exposes it as `averageRating`.


---

## Contract 03 — ERC8004ValidationRegistry (V1, contracts/)

**File**: `contracts/ERC8004ValidationRegistry.sol`  
**Role**: Validation  
**Version**: V1 (baseline, with partial audit fixes applied)

### Summary

The V1 Validation Registry implements cryptoeconomic claim validation for AI agent assertions. Validators stake ≥ 1 ETH (`MIN_VALIDATOR_STAKE`) to participate. Requesters deposit ETH with validation requests; validators vote approve or reject; the majority side earns `VALIDATION_REWARD` (0.1 ETH, fixed) while the minority is slashed the same amount. The contract also supports ZK proof verification through a `BeliefAttestationVerifier` integration. This version has already received HIGH-001 audit fixes (ReentrancyGuard, CEI pattern, `.call{}` instead of `.transfer()`). It lacks per-request escrow (added in V3 CRITICAL-VR-01), duplicate response prevention (V3 HIGH-VR-01), and deadlines (V3 MEDIUM-VR-01).

### Full Source with Line Numbers

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3:
  4: import "./PrivacyGuarantees.sol";
  5: import "./ERC8004IdentityRegistry.sol";
  6: import "./BeliefAttestationVerifier.sol";
  7: import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
  8:
  9: /// @custom:audit-fix HIGH-001 — Added ReentrancyGuard; replaced .transfer() with .call{}
 10: contract ERC8004ValidationRegistry is PrivacyGuarantees, ReentrancyGuard {
 11:
 12:     ERC8004IdentityRegistry public immutable identityRegistry;
 13:     BeliefAttestationVerifier public immutable zkVerifier;
 14:
 15:     enum ValidationType { STAKER_RERUN, ZK_PROOF, TEE_ORACLE, TRUSTED_JUDGE, MULTI_VALIDATOR }
 16:     enum ValidationStatus { PENDING, APPROVED, REJECTED, DISPUTED }
 17:
 18:     struct ValidationRequest {
 19:         uint256 requestId; address agentAddress; address requester;
 20:         string claimURI; bytes32 claimHash; ValidationType validationType;
 21:         uint256 stakeAmount; uint256 createdAt; ValidationStatus status;
 22:         uint256 validatorsRequired; uint256 approvalsCount; uint256 rejectionsCount;
 23:     }
 24:
 25:     struct ValidationResponse {
 26:         uint256 responseId; uint256 requestId; address validator;
 27:         bool approved; string evidenceURI; bytes zkProof;
 28:         uint256 timestamp; uint256 validatorStake;
 29:     }
 30:
 31:     mapping(uint256 => ValidationRequest) public validationRequests;
 32:     uint256 public nextRequestId = 1;
 33:     mapping(uint256 => uint256[]) public requestResponses;
 34:     mapping(uint256 => ValidationResponse) public validationResponses;
 35:     uint256 public nextResponseId = 1;
 36:     mapping(address => uint256) public validatorStakes;
 37:     mapping(address => uint256) public validatorActiveValidations;
 38:     mapping(address => uint256[]) public agentValidationRequests;
 39:
 40:     uint256 public constant MIN_VALIDATOR_STAKE = 1 ether;
 41:     uint256 public constant VALIDATION_REWARD = 0.1 ether;
 42:
 43:     // [events omitted for brevity — see source]
 44:
 45:     constructor(address _identityRegistry, address _zkVerifier) {
 46:         require(_identityRegistry != address(0), "Invalid identity registry");
 47:         require(_zkVerifier != address(0), "Invalid ZK verifier");
 48:         identityRegistry = ERC8004IdentityRegistry(_identityRegistry);
 49:         zkVerifier = BeliefAttestationVerifier(_zkVerifier);
 50:     }
 51:
 52:     function requestValidation(address agentAddress, string calldata claimURI,
 53:             bytes32 claimHash, ValidationType validationType,
 54:             uint256 validatorsRequired) external payable {
 55:         require(agentAddress != address(0), "Invalid agent address");
 56:         require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
 57:         require(bytes(claimURI).length > 0, "Claim URI required");
 58:         require(msg.value >= MIN_VALIDATOR_STAKE, "Insufficient stake");
 59:         if (validationType == ValidationType.MULTI_VALIDATOR)
 60:             require(validatorsRequired >= 3, "Multi-validator needs >=3 validators");
 61:         else validatorsRequired = 1;
 62:         uint256 requestId = nextRequestId++;
 63:         validationRequests[requestId] = ValidationRequest({ requestId: requestId,
 64:             agentAddress: agentAddress, requester: msg.sender, claimURI: claimURI,
 65:             claimHash: claimHash, validationType: validationType, stakeAmount: msg.value,
 66:             createdAt: block.timestamp, status: ValidationStatus.PENDING,
 67:             validatorsRequired: validatorsRequired, approvalsCount: 0, rejectionsCount: 0 });
 68:         agentValidationRequests[agentAddress].push(requestId);
 69:         emit ValidationRequested(requestId, agentAddress, msg.sender,
 70:             validationType, msg.value, block.timestamp);
 71:     }
 72:
 73:     function submitValidation(uint256 requestId, bool approved,
 74:             string calldata evidenceURI, bytes calldata zkProof)
 75:             external payable nonReentrant {
 76:         ValidationRequest storage request = validationRequests[requestId];
 77:         require(request.status == ValidationStatus.PENDING, "Request not pending");
 78:         require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE,
 79:             "Insufficient validator stake");
 80:         if (request.validationType == ValidationType.ZK_PROOF)
 81:             require(zkProof.length > 0, "ZK proof required");
 82:         uint256 responseId = nextResponseId++;
 83:         validationResponses[responseId] = ValidationResponse({ responseId: responseId,
 84:             requestId: requestId, validator: msg.sender, approved: approved,
 85:             evidenceURI: evidenceURI, zkProof: zkProof, timestamp: block.timestamp,
 86:             validatorStake: msg.value });
 87:         requestResponses[requestId].push(responseId);
 88:         validatorActiveValidations[msg.sender] += 1;
 89:         if (approved) request.approvalsCount += 1;
 90:         else request.rejectionsCount += 1;
 91:         emit ValidationResponseSubmitted(responseId, requestId, msg.sender,
 92:             approved, block.timestamp);
 93:         _checkValidationComplete(requestId);
 94:     }
 95:
 96:     function submitValidationZK(uint256 requestId, bool approved,
 97:             string calldata evidenceURI, bytes calldata proofBytes,
 98:             uint256[] calldata publicInputs) external payable nonReentrant {
 99:         ValidationRequest storage request = validationRequests[requestId];
 100:        require(request.status == ValidationStatus.PENDING, "Request not pending");
 101:        require(request.validationType == ValidationType.ZK_PROOF,
 102:            "Not a ZK validation request");
 103:        require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE,
 104:            "Insufficient validator stake");
 105:        require(proofBytes.length > 0, "ZK proof required");
 106:        bool ok = zkVerifier.verifyProof(proofBytes, publicInputs);
 107:        require(ok, "ZK proof invalid");
 108:        // [response storage, counter increments, emit, _checkValidationComplete — same as above]
 109:    }
 110:
 111:    function _checkValidationComplete(uint256 requestId) internal {
 112:        ValidationRequest storage request = validationRequests[requestId];
 113:        uint256 totalResponses = request.approvalsCount + request.rejectionsCount;
 114:        if (totalResponses >= request.validatorsRequired) {
 115:            if (request.approvalsCount > request.rejectionsCount) {
 116:                request.status = ValidationStatus.APPROVED;
 117:                _distributeRewards(requestId, true);
 118:            } else if (request.rejectionsCount > request.approvalsCount) {
 119:                request.status = ValidationStatus.REJECTED;
 120:                _distributeRewards(requestId, false);
 121:            } else { request.status = ValidationStatus.DISPUTED; }
 122:            emit ValidationCompleted(requestId, request.status, block.timestamp);
 123:        }
 124:    }
 125:
 126:    // slither-disable-next-line reentrancy-eth
 127:    function _distributeRewards(uint256 requestId, bool approved) internal {
 128:        ValidationRequest storage request = validationRequests[requestId];
 129:        uint256[] memory responses = requestResponses[requestId];
 130:        for (uint256 i = 0; i < responses.length; i++) {
 131:            ValidationResponse storage response = validationResponses[responses[i]];
 132:            address validator = response.validator;
 133:            if (response.approved == approved) {
 134:                validatorActiveValidations[validator] -= 1;  // CEI fix
 135:                (bool rewardSuccess, ) = payable(validator).call{value: VALIDATION_REWARD}("");
 136:                if (!rewardSuccess)
 137:                    emit ValidatorSlashed(validator, 0, "Reward transfer failed", block.timestamp);
 138:            } else {
 139:                uint256 slashAmount = validatorStakes[validator] >= VALIDATION_REWARD
 140:                    ? VALIDATION_REWARD : validatorStakes[validator];
 139:                validatorStakes[validator] -= slashAmount;
 140:                validatorActiveValidations[validator] -= 1;
 141:                emit ValidatorSlashed(validator, slashAmount, "Voted against majority",
 142:                    block.timestamp);
 143:            }
 144:        }
 145:    }
 146:
 147:    function stakeAsValidator() external payable nonReentrant {
 148:        require(msg.value >= MIN_VALIDATOR_STAKE, "Insufficient stake");
 149:        validatorStakes[msg.sender] += msg.value;
 150:        emit ValidatorStaked(msg.sender, msg.value, validatorStakes[msg.sender], block.timestamp);
 151:    }
 152:
 153:    function withdrawValidatorStake(uint256 amount) external nonReentrant {
 154:        require(amount > 0, "Amount must be greater than zero");
 155:        require(validatorStakes[msg.sender] >= amount, "Insufficient stake");
 156:        require(validatorActiveValidations[msg.sender] == 0, "Active validations pending");
 157:        validatorStakes[msg.sender] -= amount;  // CEI: state before external call
 158:        (bool success, ) = payable(msg.sender).call{value: amount}("");
 159:        require(success, "Stake withdrawal failed");
 160:        emit ValidatorStakeWithdrawn(msg.sender, amount, validatorStakes[msg.sender], block.timestamp);
 161:    }
 162:
 163:    // [getValidationRequest, getValidationResponses, paginated variants — view functions]
 164: }
```

### Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 7 | OpenZeppelin `ReentrancyGuard` — adds a `_status` storage variable. The `nonReentrant` modifier sets `_status = ENTERED` (2) on entry and `ENTERED = NOT_ENTERED` (1) on exit, preventing reentrant calls. |
| 15 | `ValidationType` enum: STAKER_RERUN (0), ZK_PROOF (1), TEE_ORACLE (2), TRUSTED_JUDGE (3), MULTI_VALIDATOR (4). Stored as `uint8` in the `ValidationRequest` struct — saves storage. |
| 16 | `ValidationStatus` enum: PENDING (0), APPROVED (1), REJECTED (2), DISPUTED (3). V3 adds CANCELLED (4). |
| 18–23 | `ValidationRequest`: 12 fields. Notable: `stakeAmount` stores the ETH deposited with the request but in V1 this ETH is never returned — it accumulates in the contract indefinitely because `_distributeRewards` pays validators from the contract's general balance using `VALIDATION_REWARD`, not from the specific request's deposit. This is CRITICAL-VR-01, fixed in V3. |
| 40 | `MIN_VALIDATOR_STAKE = 1 ether` = 1000000000000000000 wei. This is also the minimum escrow per request. |
| 41 | `VALIDATION_REWARD = 0.1 ether` = 100000000000000000 wei. In V1 this is paid to majority validators from the contract's general balance. If the contract lacks sufficient balance, rewards silently fail (the non-revert pattern after `.call{}`). |
| 52–71 | `requestValidation`: `payable`. The `msg.value` is stored in `stakeAmount` but NOT in a per-request escrow. `msg.value` simply increases the contract's ETH balance. Non-MULTI_VALIDATOR types force `validatorsRequired = 1`. |
| 73–94 | `submitValidation`: `payable nonReentrant`. The `payable` is HIGH-VR-03 in V3 (removed because validators should pre-stake, not pay per response). No duplicate check — same validator can submit multiple responses (HIGH-VR-01 in V3). For ZK_PROOF type, only checks that `zkProof.length > 0` — does NOT call `zkVerifier.verifyProof()`. Use `submitValidationZK` for on-chain verification. |
| 80–81 | ZK proof length check: necessary but insufficient — the bytes could be random garbage. `submitValidationZK` adds on-chain proof verification. |
| 86 | `validatorStake: msg.value` — snapshots the ETH sent with this response call, not the validator's pre-staked amount. This is misleading in V1; V3 changes this to `validatorStakes[msg.sender]`. |
| 96–109 | `submitValidationZK`: calls `zkVerifier.verifyProof(proofBytes, publicInputs)`. No binding of `publicInputs[0]` to `claimHash` (added in V3 MEDIUM-VR-03 — without this, a valid proof for a different claim could be submitted). |
| 111–124 | `_checkValidationComplete`: called after each response. Checks if `totalResponses >= validatorsRequired`. Uses strict majority — ties (equal approvals and rejections) result in DISPUTED. DISPUTED requests never trigger `_distributeRewards`, so in V1 validators who voted in a tied request are never decremented in `validatorActiveValidations` and can never withdraw their stake — this is HIGH-15, fixed in V3.3. |
| 126–145 | `_distributeRewards`: iterates `requestResponses[requestId]` array (unbounded). CEI pattern applied: `validatorActiveValidations[validator] -= 1` before the `.call{}`. Failed reward transfers are logged with `ValidatorSlashed` event but do not revert (DoS resistance). Slash uses `min(validatorStakes[validator], VALIDATION_REWARD)` to prevent underflow. |
| 153–161 | `withdrawValidatorStake`: requires `validatorActiveValidations[msg.sender] == 0` — prevents withdrawal while participating in pending validations. CEI: state update before `.call{}`. |
| 163 | Paginated view functions are present in V1 contracts/ already (added as part of the audit process). |

### State Variables

| Variable | Type | Purpose | Mutability | Who Can Change |
|----------|------|---------|------------|----------------|
| `identityRegistry` | `ERC8004IdentityRegistry` (immutable) | Agent active check | Immutable | Never |
| `zkVerifier` | `BeliefAttestationVerifier` (immutable) | ZK proof verifier | Immutable | Never |
| `validationRequests` | `mapping(uint256 => ValidationRequest)` | All requests | Mutable | `requestValidation`, `_checkValidationComplete` |
| `nextRequestId` | `uint256`, starts at 1 | Request ID counter | Monotonically increasing | `requestValidation` |
| `requestResponses` | `mapping(uint256 => uint256[])` | Request → response IDs | Append-only | `submitValidation`, `submitValidationZK` |
| `validationResponses` | `mapping(uint256 => ValidationResponse)` | All responses | Append-only | `submitValidation`, `submitValidationZK` |
| `nextResponseId` | `uint256`, starts at 1 | Response ID counter | Monotonically increasing | `submitValidation`, `submitValidationZK` |
| `validatorStakes` | `mapping(address => uint256)` | Validator ETH stakes | Mutable | `stakeAsValidator` (+), `withdrawValidatorStake` (-), `_distributeRewards` (-) |
| `validatorActiveValidations` | `mapping(address => uint256)` | Per-validator active count | Mutable | `submitValidation` (+1), `_distributeRewards` (-1) |
| `agentValidationRequests` | `mapping(address => uint256[])` | Agent → request IDs | Append-only | `requestValidation` |
| `MIN_VALIDATOR_STAKE` | `uint256` constant = 1e18 wei | Minimum stake | Immutable | Never |
| `VALIDATION_REWARD` | `uint256` constant = 1e17 wei | Fixed reward per validator (V1) | Immutable | Never |

### Functions

#### `requestValidation(address, string, bytes32, ValidationType, uint256) external payable`
- Requires `msg.value >= 1 ether`. Creates request. For non-MULTI_VALIDATOR types, forces `validatorsRequired = 1`.
- Reverts: zero address, agent not registered, empty claimURI, insufficient stake, multi-validator with < 3 required.

#### `submitValidation(uint256, bool, string, bytes) external payable nonReentrant`
- Requires pending request, validator stake ≥ 1 ETH. For ZK_PROOF type, requires non-empty proof bytes (but does NOT verify the proof on-chain).
- State changes: new response stored, `requestResponses[requestId]` appended, `validatorActiveValidations[msg.sender]++`, approval/rejection counts updated. Calls `_checkValidationComplete`.

#### `submitValidationZK(uint256, bool, string, bytes, uint256[]) external payable nonReentrant`
- As above but calls `zkVerifier.verifyProof(proofBytes, publicInputs)`. No `publicInputs[0]` binding to `claimHash` in V1.

#### `_checkValidationComplete(uint256) internal`
- Called after each response. Finalizes status when `total >= required`. Calls `_distributeRewards` for APPROVED/REJECTED. DISPUTED triggers no rewards.

#### `_distributeRewards(uint256, bool) internal`
- Iterates all responses. Majority validators: decrement active count, send `VALIDATION_REWARD` via `.call{}`. Minority validators: slash `min(stake, VALIDATION_REWARD)`, decrement active count.

#### `stakeAsValidator() external payable nonReentrant`
- Adds `msg.value` to `validatorStakes[msg.sender]`. Requires ≥ 1 ETH.

#### `withdrawValidatorStake(uint256) external nonReentrant`
- CEI: deducts from `validatorStakes`, then `.call{}`. Requires no active validations.

### Math

**Reward distribution (V1)**:
Each majority validator receives exactly `VALIDATION_REWARD = 0.1 ether`. Source: contract's general balance (not escrowed per request). The requester's deposited ETH accumulates in the contract and is never returned.

**Slash amount**:
\[
\text{slashAmount} = \min(\text{validatorStakes[validator]},\ \text{VALIDATION_REWARD})
\]
Prevents underflow if stake is < 0.1 ETH at slash time.

**Multi-validator consensus threshold**:
Request finalizes when `approvalsCount + rejectionsCount >= validatorsRequired`. Majority = strict `>`. Tie = DISPUTED.

### Events

| Event | When fired | Notes |
|-------|-----------|-------|
| `ValidationRequested` | `requestValidation` | agentAddress, requester, type, stakeAmount indexed |
| `ValidationResponseSubmitted` | `submitValidation` / `submitValidationZK` | responseId, requestId, validator |
| `ValidationCompleted` | `_checkValidationComplete` threshold reached | finalStatus |
| `ValidatorStaked` | `stakeAsValidator` | amount, totalStake |
| `ValidatorSlashed` | minority slash or failed reward | validator, amount, reason |
| `ValidatorStakeWithdrawn` | `withdrawValidatorStake` | amount, remainingStake |

### Security Invariants

- Validators must pre-stake before participating: `validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE`
- CEI pattern enforced on all ETH transfers
- `nonReentrant` on all ETH-transferring functions
- Failed ETH sends do not revert — they log and continue (prevents single-validator DoS)
- **V1 gap**: No per-request escrow — requester ETH is not returned
- **V1 gap**: Duplicate responses allowed from the same validator
- **V1 gap**: No request deadline — pending requests live forever
- **V1 gap**: DISPUTED validators are permanently locked (active count never decremented)
- **V1 gap**: ZK proof binding to claimHash not enforced in `submitValidationZK`

### ERC-8004 Standard Compliance

Implements ERC-8004 §7 (Validation), §8 (Economic Stakes). Supports all five `ValidationType` values. Provides ZK proof pathway through `BeliefAttestationVerifier`. Multi-validator consensus is implemented.

---

## Contract 04 — VaultfireERC8004Adapter (V1)

**File**: `contracts/VaultfireERC8004Adapter.sol`  
**Role**: Adapter  
**Version**: V1 (baseline)

### Summary

The V1 Adapter bridges Vaultfire's AI Partnership Bonds (V2) to the ERC-8004 registries. It provides three integration points: agent registration tracking (`registerAgentForPartnership`), bond-to-reputation synchronization (`syncPartnershipReputation`), and partnership validation requests (`requestPartnershipValidation`). It also exposes two `discoverVaultfireAgents` overloads — one typed (correct), one no-arg (has a hash mismatch bug fixed in V3 MEDIUM-AD-01). The V1 `syncPartnershipReputation` allows ANY caller to trigger reputation writes for any active bond, which is the CRITICAL-AD-01 vulnerability corrected in V3.

### Full Source with Line Numbers

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3:
  4: import "./ERC8004IdentityRegistry.sol";
  5: import "./ERC8004ReputationRegistry.sol";
  6: import "./ERC8004ValidationRegistry.sol";
  7: import "./AIPartnershipBondsV2.sol";
  8:
  9: contract VaultfireERC8004Adapter {
 10:
 11:     AIPartnershipBondsV2 public immutable partnershipBonds;
 12:     ERC8004IdentityRegistry public immutable identityRegistry;
 13:     ERC8004ReputationRegistry public immutable reputationRegistry;
 14:     ERC8004ValidationRegistry public immutable validationRegistry;
 15:
 16:     mapping(address => bool) public autoRegisteredAgents;
 17:     mapping(uint256 => bool) public bondReputationSynced;
 18:
 19:     event AgentAutoRegistered(address indexed agentAddress, string agentType, uint256 timestamp);
 20:     event PartnershipReputationSynced(uint256 indexed bondId, address indexed agentAddress,
 21:         uint256 rating, uint256 timestamp);
 22:     event ValidationRequestCreated(uint256 indexed requestId, uint256 indexed bondId,
 23:         address indexed agentAddress, uint256 timestamp);
 24:
 25:     constructor(address _partnershipBonds, address _identityRegistry,
 26:             address _reputationRegistry, address _validationRegistry) {
 27:         // [non-zero checks, immutable assignments]
 28:     }
 29:
 30:     function registerAgentForPartnership(string calldata agentURI,
 31:             string calldata agentType) external {
 32:         require(!autoRegisteredAgents[msg.sender], "Already registered for VaultFire");
 33:         require(bytes(agentURI).length > 0, "Agent URI required");
 34:         require(bytes(agentType).length > 0, "Agent type required");
 35:         if (!identityRegistry.isAgentActive(msg.sender))
 36:             revert("Agent must call identityRegistry.registerAgent() first");
 37:         // capabilitiesHash computed but not stored — V1 logic artifact
 38:         bytes32 capabilitiesHash = keccak256(abi.encodePacked("vaultfire-ai-partnership", agentType));
 39:         capabilitiesHash; // suppress unused warning
 40:         autoRegisteredAgents[msg.sender] = true;
 41:         emit AgentAutoRegistered(msg.sender, agentType, block.timestamp);
 42:     }
 43:
 44:     function syncPartnershipReputation(uint256 bondId) external {
 45:         require(!bondReputationSynced[bondId], "Already synced");
 46:         (, address human, address aiAgent, string memory partnershipType, , , , , bool active)
 47:             = partnershipBonds.bonds(bondId);
 48:         require(active, "Bond not active");
 49:         require(aiAgent != address(0), "Invalid agent address");
 50:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered");
 51:         uint256 rating = _calculatePartnershipRating(bondId);
 52:         reputationRegistry.submitFeedback(aiAgent, rating, "partnership_quality",
 53:             "", true, bondId);  // V1 BUG: any caller can set verified=true
 54:         bondReputationSynced[bondId] = true;  // V1 BUG: set AFTER external call (CEI violation)
 55:         emit PartnershipReputationSynced(bondId, aiAgent, rating, block.timestamp);
 56:     }
 57:
 58:     function _calculatePartnershipRating(uint256 bondId) internal view returns (uint256) {
 59:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 60:         return qualityScore;  // V1: no bounds check — qualityScore could exceed 10000
 61:     }
 62:
 63:     function requestPartnershipValidation(uint256 bondId, string calldata claimURI,
 64:             ERC8004ValidationRegistry.ValidationType validationType) external payable {
 65:         (, , address aiAgent, , , , , , bool active) = partnershipBonds.bonds(bondId);
 66:         require(active, "Bond not active");
 67:         require(aiAgent != address(0), "Invalid agent address");
 68:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered");
 69:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 70:         bytes32 claimHash = keccak256(abi.encodePacked(bondId, qualityScore, block.timestamp));
 71:         // V1 BUG: block.timestamp in claimHash makes it non-deterministic (HIGH-AD-01 in V3)
 72:         validationRegistry.requestValidation{value: msg.value}(aiAgent, claimURI,
 73:             claimHash, validationType,
 74:             validationType == ERC8004ValidationRegistry.ValidationType.MULTI_VALIDATOR ? 3 : 1);
 75:         // V1: no emit with requestId (added in V3)
 76:     }
 77:
 78:     function getAgentCrossPlatformReputation(address agentAddress) external view
 79:             returns (uint256 vaultfireRating, uint256 erc8004Rating,
 80:                      uint256 totalFeedbacks, uint256 verifiedPercentage) {
 81:         vaultfireRating = 0; // placeholder
 82:         (erc8004Rating, totalFeedbacks, , ) = reputationRegistry.getReputation(agentAddress);
 83:         verifiedPercentage = reputationRegistry.getVerifiedFeedbackPercentage(agentAddress);
 84:     }
 85:
 86:     function isAgentFullyRegistered(address agentAddress) external view
 87:             returns (bool registeredERC8004, bool registeredVaultFire) {
 88:         registeredERC8004 = identityRegistry.isAgentActive(agentAddress);
 89:         registeredVaultFire = autoRegisteredAgents[agentAddress];
 90:     }
 91:
 92:     function discoverVaultfireAgents(string calldata agentType)
 93:             external view returns (address[] memory) {
 94:         bytes32 capabilitiesHash = keccak256(abi.encodePacked("vaultfire-ai-partnership", agentType));
 95:         return identityRegistry.discoverAgentsByCapability(capabilitiesHash);
 96:     }
 97:
 98:     function discoverVaultfireAgents() external view returns (address[] memory) {
 99:         // BUG: hash = keccak256("vaultfire-ai-partnership") — no agentType suffix
 100:        // but agents register with "vaultfire-ai-partnership" + agentType hash
 101:        // → always returns empty array
 102:        bytes32 capabilitiesHash = keccak256(abi.encodePacked("vaultfire-ai-partnership"));
 103:        return identityRegistry.discoverAgentsByCapability(capabilitiesHash);
 104:    }
 105: }
```

### Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 11–14 | Four `immutable` contract references. Set once in constructor. The adapter is the single bridge between the bond system and the ERC-8004 layer. |
| 16–17 | `autoRegisteredAgents`: tracks which agents have called `registerAgentForPartnership`. Separate from identity registry registration. `bondReputationSynced`: prevents double-syncing a bond's reputation. |
| 30–42 | `registerAgentForPartnership`: two-step registration flow. Agent must first call `identityRegistry.registerAgent()` directly (since the identity registry uses `msg.sender` as the agent address — the adapter cannot register on behalf of another). Then the agent calls this function. The `capabilitiesHash` is computed but immediately discarded with `capabilitiesHash;` — a dead computation included for documentation clarity. |
| 44–56 | `syncPartnershipReputation`: **CRITICAL-AD-01 in V1** — any caller can call this for any active bond. The function reads bond data from `partnershipBonds.bonds(bondId)` using a tuple destructuring. The `human` variable is extracted but never checked against `msg.sender`. Line 52–53 calls `reputationRegistry.submitFeedback` with `verified=true` — any caller can create "verified" reputation entries. Line 54: **CEI violation** — `bondReputationSynced[bondId] = true` is set AFTER the external call to `reputationRegistry.submitFeedback`. A malicious reputation registry could reenter `syncPartnershipReputation` before line 54 is reached, bypassing the `require(!bondReputationSynced[bondId])` guard and submitting multiple reputation entries for the same bond. Fixed in V3 by moving the state update before the external call. |
| 58–61 | `_calculatePartnershipRating`: reads `partnershipQualityScore(bondId)` and returns it directly. No bounds check in V1 — if the bond contract returns a score > 10000, the reputation registry would reject it with "Rating must be 0-10000" but the adapter doesn't validate defensively. Bounds check added in V3 (MEDIUM-AD-02). |
| 63–76 | `requestPartnershipValidation`: line 70 includes `block.timestamp` in `claimHash`. This makes the hash non-deterministic — calling the function twice for the same bond in different blocks produces different hashes, preventing consistent claim verification. Removed in V3 (HIGH-AD-01). No deduplication guard — can call for same bond multiple times. Guard added in V3 (HIGH-AD-02). |
| 92–96 | `discoverVaultfireAgents(string)`: correct implementation — hashes "vaultfire-ai-partnership" + agentType, matches the hash stored during registration. |
| 98–104 | `discoverVaultfireAgents()` (no arg): **MEDIUM-AD-01 bug** — hashes only "vaultfire-ai-partnership" without agentType suffix. But agents registered with a type-specific hash never appear under this key. Always returns empty array. Removed in V3. |

### State Variables

| Variable | Type | Purpose | Mutability | Who Can Change |
|----------|------|---------|------------|----------------|
| `partnershipBonds` | `AIPartnershipBondsV2` (immutable) | Bond data source | Immutable | Never |
| `identityRegistry` | `ERC8004IdentityRegistry` (immutable) | Agent active check | Immutable | Never |
| `reputationRegistry` | `ERC8004ReputationRegistry` (immutable) | Reputation writes | Immutable | Never |
| `validationRegistry` | `ERC8004ValidationRegistry` (immutable) | Validation requests | Immutable | Never |
| `autoRegisteredAgents` | `mapping(address => bool)` | VaultFire partnership flag | Mutable | `registerAgentForPartnership` |
| `bondReputationSynced` | `mapping(uint256 => bool)` | Bond sync deduplication | Mutable | `syncPartnershipReputation` |

### Functions

#### `syncPartnershipReputation(uint256 bondId) external`
- **V1 issues**: open to any caller; CEI violation; no caller check against bond participants
- Reads bond data via 9-field tuple destructure from `partnershipBonds.bonds(bondId)`
- Calls `reputationRegistry.submitFeedback(..., true, bondId)`

#### `requestPartnershipValidation(uint256, string, ValidationType) external payable`
- Forwards ETH to `validationRegistry.requestValidation{value: msg.value}`
- claimHash includes non-deterministic `block.timestamp`
- No deduplication

#### `discoverVaultfireAgents(string) external view`
- Correct: delegates to `identityRegistry.discoverAgentsByCapability(keccak256("vaultfire-ai-partnership" + agentType))`

#### `discoverVaultfireAgents() external view`
- Buggy: always returns empty array — hash mismatch

### Security Invariants

- `bondReputationSynced[bondId]` prevents double-syncing but is set after external call (CEI violation in V1)
- Agent must be active in identity registry before reputation sync
- No ETH is held by adapter in V1 (no `receive()`, no ETH rescue — added in V3)
- **V1 gap**: no access control on `syncPartnershipReputation`
- **V1 gap**: `_calculatePartnershipRating` has no bounds check

### ERC-8004 Standard Compliance

Implements ERC-8004 §9 (Interoperability). Bridges VaultFire bond quality scores into portable ERC-8004 reputation. Provides cross-platform reputation query (`getAgentCrossPlatformReputation`).


---

## Contract 05 — ERC8004IdentityRegistryV3 (V3 early, v3/)

**File**: `v3/ERC8004IdentityRegistryV3.sol`  
**Role**: Identity  
**Version**: V3 early

### Summary

The first V3 iteration of the Identity Registry. Applies the following audit findings versus V1: HIGH-IR-01 removes the `registeredAgents` unbounded array and replaces it with a `totalRegisteredAgents` counter; HIGH-IR-02 removes the `agentsByCapability` unbounded array; MEDIUM-IR-01 allows re-registration after deactivation (increments `registrationCount`, only increments global counter on first registration), and clears `capabilitiesHash` on deactivation; MEDIUM-IR-02 validates URI scheme (`https://` or `ipfs://`) and caps length at 512 bytes; LOW-IR-01 rejects zero `capabilitiesHash`; LOW-IR-03 caps `agentType` at 100 bytes. Adds an `owner` role with `transferOwnership` and emergency `forceDeactivate`. The logic is identical to the V3-PR-ready and V3.3-audit versions — the only difference is the import paths and the presence/absence of the `owner` field in the V3.3 audit version (which removed it from the struct, keeping it as a standalone state variable outside the struct).

### Full Source with Line Numbers

*(Contract ~280 lines — full source shown in condensed form with all logic preserved)*

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: import "../PrivacyGuarantees.sol";
  4: import "../MissionEnforcement.sol";
  5:
  6: contract ERC8004IdentityRegistryV3 is PrivacyGuarantees, MissionEnforcement {
  7:
  8:     // ── Owner role ──────────────────────────────────────────────────────────
  9:     address public owner;
 10:     event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
 11:     modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
 12:
 13:     // ── AgentIdentity struct ─────────────────────────────────────────────────
 14:     struct AgentIdentity {
 15:         address agentAddress; string agentURI; uint256 registeredAt;
 16:         bool active; string agentType; bytes32 capabilitiesHash;
 17:     }
 18:
 19:     mapping(address => AgentIdentity) public agents;
 20:
 21:     /// @custom:audit-v3 HIGH-IR-01: counter replaces array
 22:     uint256 public totalRegisteredAgents;
 23:
 24:     /// @custom:audit-v3 MEDIUM-IR-01: per-agent registration count
 25:     mapping(address => uint256) public registrationCount;
 26:
 27:     // ── Events ──────────────────────────────────────────────────────────────
 28:     event AgentRegistered(address indexed agentAddress, string agentURI, string agentType,
 29:         bytes32 indexed capabilitiesHash, uint256 timestamp);  // capabilitiesHash NOW indexed
 30:     event AgentUpdated(address indexed agentAddress, string newAgentURI, uint256 timestamp);
 31:     event AgentDeactivated(address indexed agentAddress, uint256 timestamp);
 32:     event AgentForceDeactivated(address indexed agentAddress, address indexed admin,
 33:         uint256 timestamp);
 34:
 35:     constructor() { owner = msg.sender; }
 36:
 37:     function transferOwnership(address newOwner) external onlyOwner {
 38:         require(newOwner != address(0), "Invalid new owner");
 39:         emit OwnershipTransferred(owner, newOwner);
 40:         owner = newOwner;
 41:     }
 42:
 43:     // ── URI validation ────────────────────────────────────────────────────
 44:     function _validateURI(string calldata uri) internal pure {
 45:         bytes memory b = bytes(uri);
 45:         require(b.length > 0, "URI required");
 46:         require(b.length <= 512, "URI too long (max 512 bytes)");
 47:         bool validScheme = false;
 48:         if (b.length >= 8) {  // check "https://"
 49:             if (b[0]==0x68 && b[1]==0x74 && b[2]==0x74 && b[3]==0x70 &&
 50:                 b[4]==0x73 && b[5]==0x3A && b[6]==0x2F && b[7]==0x2F)
 51:                 validScheme = true;
 52:         }
 53:         if (!validScheme && b.length >= 7) {  // check "ipfs://"
 54:             if (b[0]==0x69 && b[1]==0x70 && b[2]==0x66 && b[3]==0x73 &&
 55:                 b[4]==0x3A && b[5]==0x2F && b[6]==0x2F)
 56:                 validScheme = true;
 57:         }
 58:         require(validScheme, "URI must start with https:// or ipfs://");
 59:     }
 60:
 61:     function registerAgent(string calldata agentURI, string calldata agentType,
 62:             bytes32 capabilitiesHash) external {
 63:         require(bytes(agentType).length > 0, "Agent type required");
 64:         require(bytes(agentType).length <= 100, "Agent type too long (max 100 bytes)");
 65:         require(capabilitiesHash != bytes32(0), "Capabilities hash required");
 66:         _validateURI(agentURI);
 67:         AgentIdentity storage identity = agents[msg.sender];
 68:         require(!identity.active, "Agent already registered and active");
 69:         bool isFirstRegistration = (registrationCount[msg.sender] == 0);
 70:         identity.agentAddress = msg.sender; identity.agentURI = agentURI;
 71:         identity.registeredAt = block.timestamp; identity.active = true;
 72:         identity.agentType = agentType; identity.capabilitiesHash = capabilitiesHash;
 73:         registrationCount[msg.sender] += 1;
 74:         if (isFirstRegistration) totalRegisteredAgents += 1;
 75:         emit AgentRegistered(msg.sender, agentURI, agentType, capabilitiesHash, block.timestamp);
 76:     }
 77:
 78:     function updateAgentURI(string calldata newAgentURI) external {
 79:         require(agents[msg.sender].active, "Agent not registered");
 80:         _validateURI(newAgentURI);
 81:         agents[msg.sender].agentURI = newAgentURI;
 82:         emit AgentUpdated(msg.sender, newAgentURI, block.timestamp);
 83:     }
 84:
 85:     function deactivateAgent() external {
 86:         require(agents[msg.sender].active, "Agent not registered");
 87:         agents[msg.sender].active = false;
 88:         agents[msg.sender].capabilitiesHash = bytes32(0);  // MEDIUM-IR-01: clear hash
 89:         emit AgentDeactivated(msg.sender, block.timestamp);
 90:     }
 91:
 92:     function forceDeactivate(address agentAddress) external onlyOwner {
 93:         require(agentAddress != address(0), "Invalid agent address");
 94:         require(agents[agentAddress].active, "Agent not active");
 95:         agents[agentAddress].active = false;
 96:         agents[agentAddress].capabilitiesHash = bytes32(0);
 97:         emit AgentForceDeactivated(agentAddress, msg.sender, block.timestamp);
 98:         emit AgentDeactivated(agentAddress, block.timestamp);
 99:     }
100:
101:    function getAgent(address agentAddress) external view
102:            returns (string memory agentURI, bool active, string memory agentType,
103:                     uint256 registeredAt) {
104:        AgentIdentity memory identity = agents[agentAddress];
105:        return (identity.agentURI, identity.active, identity.agentType, identity.registeredAt);
106:    }
107:
108:    function getTotalAgents() external view returns (uint256 count) {
109:        return totalRegisteredAgents;  // HIGH-IR-01: counter, not array length
110:    }
111:
112:    function isAgentActive(address agentAddress) external view returns (bool) {
113:        return agents[agentAddress].active;
114:    }
115:
116:    function getRegistrationCount(address agentAddress) external view returns (uint256) {
117:        return registrationCount[agentAddress];
118:    }
119: }
```

### Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 9–11 | Single-step ownership. In this version `transferOwnership` immediately sets `owner = newOwner`. V3.3 (HIGH-08) changes this to a two-step pattern. |
| 22 | `totalRegisteredAgents`: counts unique addresses that have ever registered. Incremented only on first registration (line 74). Re-registration after deactivation does NOT increment this counter. This is intentional — the counter reflects unique agent identities, not total registration events. |
| 25 | `registrationCount[address]`: zero means never registered. >1 means the agent has re-registered at least once after deactivating. Used externally to detect agents with a history of deactivation/re-registration. |
| 28–29 | `AgentRegistered` event now has `capabilitiesHash` as `indexed`. This is the key enabler for off-chain discovery — external indexers can filter `AgentRegistered` events by `capabilitiesHash` to find agents by capability without any on-chain array. |
| 44–59 | `_validateURI`: byte-level comparison of URI prefix. `https://` = bytes `68 74 74 70 73 3A 2F 2F` (ASCII h-t-t-p-s-:-/-/). `ipfs://` = bytes `69 70 66 73 3A 2F 2F` (ASCII i-p-f-s-:-/-/). Uses `calldata` parameter — no memory allocation needed. `pure` function — no state reads. Maximum URI length 512 bytes protects against storage griefing. |
| 61–76 | `registerAgent` — full validation chain: agentType non-empty, ≤ 100 bytes; capabilitiesHash non-zero; URI scheme valid. Then checks `!identity.active` — deactivated agents can re-register (MEDIUM-IR-01). `isFirstRegistration` flag controls global counter increment. Writing directly to `storage identity` is gas-efficient (one slot calculation). |
| 85–90 | `deactivateAgent`: clears `capabilitiesHash = bytes32(0)` — MEDIUM-IR-01 fix. This ensures that after deactivation, any off-chain index that searches for `capabilitiesHash == X` and then calls `isAgentActive` will correctly see `false`. Without this clear, an agent could deactivate but their capabilities hash would still appear "valid" in any stored mapping. |
| 92–99 | `forceDeactivate`: owner-only emergency kill. Emits both `AgentForceDeactivated` (includes admin address) and `AgentDeactivated` (for compatibility with code that watches only the standard event). |

### State Variables

Same as V1 except:
- `registeredAgents address[]` → **removed**; replaced by `totalRegisteredAgents uint256`
- `agentsByCapability mapping(bytes32 => address[])` → **removed**; replaced by indexed event
- Added: `owner address`, `registrationCount mapping(address => uint256)`

### Math

`totalRegisteredAgents` increment condition: `registrationCount[msg.sender] == 0` before increment. This uses the pre-increment snapshot pattern — set `isFirstRegistration` before the `+= 1`, then branch on `isFirstRegistration`.

### Security Invariants

- URI validation rejects non-`https://` and non-`ipfs://` URIs — prevents arbitrary data injection
- Zero `capabilitiesHash` rejected — prevents agents from registering with no discoverable capability
- `forceDeactivate` restricted to `owner` — prevents griefing
- Re-registration after deactivation is intentional and safe: the stale capability hash is cleared first
- V3 removes on-chain arrays so there is no on-chain discovery function — all capability discovery happens off-chain via event logs

### ERC-8004 Standard Compliance

Same as V1 (§3, §4) but with improved gas safety for §4 (Discovery) — replaces on-chain arrays with event-indexed discovery, which is more scalable.

---

## Contract 06 — ERC8004ReputationRegistryV3 (V3 early, v3/)

**File**: `v3/ERC8004ReputationRegistryV3.sol`  
**Role**: Reputation  
**Version**: V3 early

### Summary

The first V3 iteration of the Reputation Registry. Introduces the `owner` and `authorizedCallers` access control model: `submitFeedback` (public, unverified) and `submitVerifiedFeedback` (authorized, verified) are split into separate functions — fixing HIGH-RR-01. Adds per-(reviewer, agent) rate limiting with a 1-hour cooldown — fixing HIGH-RR-02. Implements the Bayesian prior on the first feedback (seed to 5000 before incorporating the real rating) — fixing MEDIUM-RR-01. Adds string length caps (64 bytes category, 512 bytes feedbackURI) — fixing LOW-RR-01. Allows zero `feedbackURIHash` in the hashed path — fixing LOW-RR-03. Adds four feedback submission paths: plaintext/authorized, plaintext/public, hashed/authorized, hashed/public. The logic is functionally identical to the V3-PR-ready version.

### Key Additions vs V1

| Audit ID | Change |
|----------|--------|
| HIGH-RR-01 | `submitFeedback` (public, unverified only) + `submitVerifiedFeedback` (authorized, sets verified=true) |
| HIGH-RR-02 | `lastFeedbackTime[reviewer][agent]` mapping; `FEEDBACK_COOLDOWN = 1 hours` |
| MEDIUM-RR-01 | Bayesian prior: `averageRating = (5000 + newRating) / 2` on first feedback |
| LOW-RR-01 | `require(bytes(category).length <= 64)` and `require(bytes(feedbackURI).length <= 512)` |
| LOW-RR-03 | `feedbackURIHash` may be `bytes32(0)` in hashed path |
| Owner/auth | `owner`, `transferOwnership`, `setAuthorizedCaller`, `authorizedCallers` mapping |

### Full Source with Line Numbers (critical sections)

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: import "../PrivacyGuarantees.sol";
  4: import "./ERC8004IdentityRegistryV3.sol";
  5:
  6: contract ERC8004ReputationRegistryV3 is PrivacyGuarantees {
  7:
  8:     ERC8004IdentityRegistryV3 public immutable identityRegistry;
  9:     address public owner;
 10:     event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
 11:     modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
 12:
 13:     mapping(address => bool) public authorizedCallers;
 14:     event AuthorizedCallerSet(address indexed caller, bool authorized);
 15:     modifier onlyAuthorized() { require(authorizedCallers[msg.sender], "Not authorized"); _; }
 16:
 17:     // HIGH-RR-02: rate limiting per (reviewer, agent)
 18:     mapping(address => mapping(address => uint256)) public lastFeedbackTime;
 19:     uint256 public constant FEEDBACK_COOLDOWN = 1 hours;  // = 3600 seconds
 20:
 21:     // [Feedback, FeedbackHashed, AgentReputation structs — identical to V1]
 22:     // [feedback mappings, counter mappings, array mappings — identical to V1]
 23:
 24:     constructor(address _identityRegistry) {
 25:         require(_identityRegistry != address(0), "Invalid identity registry");
 26:         identityRegistry = ERC8004IdentityRegistryV3(_identityRegistry);
 27:         owner = msg.sender;
 28:     }
 29:
 30:     function setAuthorizedCaller(address caller, bool authorized) external onlyOwner {
 31:         require(caller != address(0), "Invalid caller address");
 32:         authorizedCallers[caller] = authorized;
 33:         emit AuthorizedCallerSet(caller, authorized);
 34:     }
 35:
 36:     // HIGH-RR-01: public path — always unverified
 37:     function submitFeedback(address agentAddress, uint256 rating,
 38:             string calldata category, string calldata feedbackURI) external {
 39:         require(identityRegistry.isAgentActive(agentAddress), "Agent not registered");
 40:         require(rating <= 10000, "Rating must be 0-10000");
 40:         require(bytes(category).length > 0, "Category required");
 41:         require(bytes(category).length <= 64, "Category too long (max 64 bytes)");
 42:         require(bytes(feedbackURI).length <= 512, "FeedbackURI too long (max 512 bytes)");
 43:         // HIGH-RR-02: rate limit check
 44:         require(block.timestamp >= lastFeedbackTime[msg.sender][agentAddress] + FEEDBACK_COOLDOWN,
 45:             "Rate limit: wait 1 hour between feedbacks");
 46:         lastFeedbackTime[msg.sender][agentAddress] = block.timestamp;
 47:         uint256 feedbackId = nextFeedbackId++;
 48:         feedbacks[feedbackId] = Feedback({ reviewer: msg.sender, agentAddress: agentAddress,
 49:             timestamp: block.timestamp, rating: rating, category: category,
 50:             feedbackURI: feedbackURI, verified: false, bondId: 0 });
 51:         agentFeedbacks[agentAddress].push(feedbackId);
 52:         reviewerFeedbacks[msg.sender].push(feedbackId);
 53:         _updateReputation(agentAddress, rating, false);
 54:         emit FeedbackSubmitted(feedbackId, msg.sender, agentAddress, rating,
 55:             category, false, 0, block.timestamp);
 56:     }
 57:
 58:     // HIGH-RR-01: authorized path — sets verified=true
 59:     function submitVerifiedFeedback(address agentAddress, uint256 rating,
 60:             string calldata category, string calldata feedbackURI,
 61:             uint256 bondId) external onlyAuthorized {
 62:         // [same validations as submitFeedback plus HIGH-RR-02 rate limit]
 63:         // sets verified=true, bondId=bondId in stored struct
 64:         // calls _updateReputation(agentAddress, rating, true)
 65:     }
 66:
 67:     // [submitFeedbackHashed, submitVerifiedFeedbackHashed — similar split]
 68:
 69:     uint256 private constant MAX_FEEDBACKS_FOR_TRUE_AVG = 1000;
 70:
 70:     function _updateReputation(address agentAddress, uint256 newRating,
 71:             bool verified) internal {
 72:         AgentReputation storage rep = reputations[agentAddress];
 73:         // MEDIUM-RR-01: Bayesian prior on first feedback
 74:         if (rep.totalFeedbacks == 0) {
 75:             rep.averageRating = (5000 + newRating) / 2;
 76:             rep.totalFeedbacks = 1;
 77:         } else if (rep.totalFeedbacks < MAX_FEEDBACKS_FOR_TRUE_AVG) {
 78:             uint256 totalRating = rep.averageRating * rep.totalFeedbacks;
 79:             totalRating += newRating;
 80:             rep.totalFeedbacks += 1;
 81:             rep.averageRating = totalRating / rep.totalFeedbacks;
 82:         } else {
 83:             rep.averageRating = (rep.averageRating * 9 + newRating) / 10;
 84:             rep.totalFeedbacks += 1;
 85:         }
 86:         if (verified) rep.verifiedFeedbacks += 1;
 87:         rep.lastUpdated = block.timestamp;
 88:         emit ReputationUpdated(agentAddress, rep.averageRating, rep.totalFeedbacks, block.timestamp);
 89:     }
 90:     // [view functions — identical to V1]
 91: }
```

### Line-by-Line Walkthrough (new additions only)

| Lines | Explanation |
|-------|-------------|
| 13–15 | `authorizedCallers` mapping controlled by `owner`. The `onlyAuthorized` modifier checks this. The Adapter contract must be granted authorization before `syncPartnershipReputation` can write verified feedback. |
| 18–19 | `lastFeedbackTime[reviewer][agent]`: nested mapping. Tracks the last `block.timestamp` a given reviewer submitted feedback for a given agent. `FEEDBACK_COOLDOWN = 3600` (one hour in seconds). |
| 44–46 | Rate limit check: `block.timestamp >= lastFeedbackTime + 3600`. Integer comparison — correct. On the very first feedback by a reviewer for an agent, `lastFeedbackTime[reviewer][agent] == 0`, so `0 + 3600 = 3600`, which is always less than any realistic `block.timestamp` (Unix epoch is ~1.7 billion), so the first feedback always passes. |
| 74–76 | **Bayesian prior** (MEDIUM-RR-01): On `totalFeedbacks == 0`, compute `averageRating = (5000 + newRating) / 2`, then set `totalFeedbacks = 1`. This weights the first real rating against a neutral prior of 5000 (midpoint of 0–10000). Effect: a single perfect (10000) rating gives `(5000 + 10000) / 2 = 7500` instead of 10000. A single terrible (0) rating gives `(5000 + 0) / 2 = 2500` instead of 0. This prevents cold-start manipulation where a single extreme first review dominates. |
| 77–82 | Phase 2 (1 ≤ n < 1000): running average. Note that after Phase 1, `totalFeedbacks = 1` and `averageRating = (5000 + firstRating) / 2`. The second feedback uses this as the starting average. |

### Bayesian Prior Math — Worked Example

Agent receives three feedbacks: 9000, 2000, 8000.

| n (before) | Rating | Phase | Computation | averageRating |
|-----------|--------|-------|-------------|---------------|
| 0 | 9000 | Bayesian | (5000 + 9000) / 2 | 7000 |
| 1 | 2000 | Running avg | (7000 × 1 + 2000) / 2 = 9000 / 2 | 4500 |
| 2 | 8000 | Running avg | (4500 × 2 + 8000) / 3 = 17000 / 3 | 5666 |

Compare V1 (no prior): 9000 → 9000 → (9000+2000)/2=5500 → (5500×2+8000)/3=6333. The V3 prior makes the first review less extreme.

### Rate Limiting — Edge Cases

- **First feedback**: `lastFeedbackTime == 0`, so `0 + 3600 = 3600 < block.timestamp` always passes
- **Same block, different agents**: allowed — the mapping is per (reviewer, agent) pair
- **Authorized callers in V3-early**: subject to the same 1-hour rate limit as public callers (changed in V3.2 MEDIUM-RR-03 to exempt authorized callers, then reversed in V3.3 HIGH-09 to re-add rate limiting for authorized callers)

### Security Invariants

- Public `submitFeedback` always writes `verified=false` and `bondId=0` — cannot be used to inflate verified counts
- `submitVerifiedFeedback` requires `authorizedCallers[msg.sender] == true` — controlled by `owner`
- Rate limiting prevents rapid-fire feedback spam (1 feedback per hour per reviewer per agent)
- **V3-early gap**: paginated view functions absent (added in V3.2 HIGH-RR-03)
- **V3-early gap**: Sybil bypass — unverified reviewer does not need to be registered (fixed in V3.3 HIGH-10)

---

## Contract 07 — ERC8004ValidationRegistryV3 (V3 early, v3/)

**File**: `v3/ERC8004ValidationRegistryV3.sol`  
**Role**: Validation  
**Version**: V3 early

### Summary

The first V3 iteration of the Validation Registry. Applies the full set of CRITICAL and HIGH audit fixes from the partner-ready audit. Key changes: per-request escrow replaces fixed `VALIDATION_REWARD` payouts (CRITICAL-VR-01); duplicate response prevention via `hasValidated[requestId][validator]` mapping (HIGH-VR-01); `payable` removed from `submitValidation`/`submitValidationZK` to force pre-staking (HIGH-VR-03); 7-day request deadline and `cancelRequest` for escrow recovery (MEDIUM-VR-01); safe decrement guard on `validatorActiveValidations` (MEDIUM-VR-02); `publicInputs[0]` bound to `request.claimHash` in ZK path (MEDIUM-VR-03). Adds `CANCELLED` status to the `ValidationStatus` enum. Logic is essentially the same as V3-PR-ready.

### Key Additions vs V1

| Audit ID | Change |
|----------|--------|
| CRITICAL-VR-01 | `requestEscrow[requestId] = msg.value`; rewards = `escrow / majorityCount`; unspent escrow returned to requester |
| HIGH-VR-01 | `hasValidated[requestId][validator]` — prevents same validator from voting twice |
| HIGH-VR-03 | `submitValidation` and `submitValidationZK` no longer `payable` |
| MEDIUM-VR-01 | `deadline = block.timestamp + 7 days` set at request time; `cancelRequest` after deadline |
| MEDIUM-VR-02 | `if (validatorActiveValidations[v] > 0) validatorActiveValidations[v] -= 1` |
| MEDIUM-VR-03 | `require(publicInputs[0] == uint256(request.claimHash))` before ZK verify |

### Critical Source Sections

```solidity
// Per-request escrow mapping (CRITICAL-VR-01)
mapping(uint256 => uint256) public requestEscrow;

// Duplicate prevention (HIGH-VR-01)
mapping(uint256 => mapping(address => bool)) public hasValidated;

// Deadline constant (MEDIUM-VR-01)
uint256 public constant REQUEST_DEADLINE = 7 days;  // = 604800 seconds

// CANCELLED status in enum
enum ValidationStatus { PENDING, APPROVED, REJECTED, DISPUTED, CANCELLED }

// requestValidation stores escrow:
requestEscrow[requestId] = msg.value;
validationRequests[requestId] = ValidationRequest({
    ...
    deadline: block.timestamp + REQUEST_DEADLINE,
    ...
});

// submitValidation — not payable, duplicate check:
require(!hasValidated[requestId][msg.sender], "Already submitted response");
hasValidated[requestId][msg.sender] = true;

// submitValidationZK — claimHash binding:
require(publicInputs.length > 0, "Public inputs required");
require(publicInputs[0] == uint256(request.claimHash), "publicInputs[0] must equal claimHash");

// cancelRequest:
function cancelRequest(uint256 requestId) external nonReentrant {
    require(request.requester == msg.sender, "Not the requester");
    require(request.status == ValidationStatus.PENDING, "Request not pending");
    require(block.timestamp > request.deadline, "Deadline not reached");
    uint256 escrow = requestEscrow[requestId];
    request.status = ValidationStatus.CANCELLED;
    requestEscrow[requestId] = 0;
    emit ValidationRequestCancelled(requestId, msg.sender, escrow, block.timestamp);
    if (escrow > 0) {
        (bool success, ) = payable(msg.sender).call{value: escrow}("");
        require(success, "Escrow return failed");
    }
}

// _distributeRewards — escrow-based:
uint256 escrow = requestEscrow[requestId];
requestEscrow[requestId] = 0;  // CEI: clear before transfers
uint256 rewardPerValidator = majorityCount > 0 ? escrow / majorityCount : 0;
// ... pay majority, slash minority, return unspent to requester
```

### Reward Math — Worked Example

Scenario: 3-validator multi-validator request, 2 ETH escrowed, 2 approve, 1 reject (majority approves).

- `majorityCount = 2`
- `rewardPerValidator = 2 ETH / 2 = 1 ETH`
- Each approving validator receives 1 ETH
- Rejecting validator: `slashAmount = min(validatorStakes[v], 0.1 ETH) = 0.1 ETH`
- `totalRewarded = 2 ETH` (assuming both transfers succeed)
- `unspent = 2 ETH - 2 ETH = 0` → nothing returned to requester

Scenario: 1-validator request, 2 ETH escrowed, validator votes approve (majority = 1):

- `rewardPerValidator = 2 ETH / 1 = 2 ETH`
- Validator receives 2 ETH
- No minority validators
- Requester gets 0 back

Scenario: 2 ETH escrowed, reward transfer fails (validator contract rejects ETH):

- `totalRewarded -= rewardPerValidator` (reward subtracted from tracker)
- `unspent = 2 ETH - 0 = 2 ETH` → returned to requester
- `ValidatorSlashed` event emitted for the validator with 0 amount and "Reward transfer failed" reason

### Security Invariants (additions over V1)

- Same validator cannot vote twice on same request (hasValidated guard)
- Escrow is per-request and isolated — one request's funds cannot leak to another
- CEI on escrow: `requestEscrow[requestId] = 0` before any external ETH transfers
- ZK proofs are bound to the specific request's claimHash — prevents cross-claim proof reuse
- Requests expire after 7 days — requester can recover escrow from stale requests
- `validatorActiveValidations` decrement is guarded against underflow: `if (v > 0) v -= 1`
- **V3-early gap**: DISPUTED requests still do NOT decrement `validatorActiveValidations` for participants (fixed in V3.3 HIGH-15)
- **V3-early gap**: `cancelRequest` does NOT decrement `validatorActiveValidations` for validators who already responded (fixed in V3.3 HIGH-19)
- **V3-early gap**: In `submitValidation`, ZK_PROOF type still stores proof bytes but allows submission without calling `zkVerifier.verifyProof()` — changed to a hard revert in V3.2 (MEDIUM-VR-04)


---

## Contract 08 — ERC8004IdentityRegistryV3 (V3 PR-ready)

**File**: `v3-pr-ready/contracts/v3/ERC8004IdentityRegistryV3.sol`  
**Role**: Identity  
**Version**: V3 PR-ready

### Summary

Functionally identical to Contract 05 (V3 early). The only difference is import paths: this version imports from `../contracts/PrivacyGuarantees.sol` and `../contracts/MissionEnforcement.sol` (relative path for the PR-ready directory layout) rather than `../PrivacyGuarantees.sol`. All logic, state variables, events, functions, and security invariants are the same. See Contract 05 for the full technical reference.

**Import paths differ:**
- V3-early: `import "../PrivacyGuarantees.sol"`, `import "../MissionEnforcement.sol"`
- V3-PR-ready: `import "../contracts/PrivacyGuarantees.sol"`, `import "../contracts/MissionEnforcement.sol"`

All other source lines are byte-for-byte identical. This contract is not separately documented to avoid duplication — see Contract 05.

---

## Contract 09 — ERC8004ReputationRegistryV3 (V3 PR-ready)

**File**: `v3-pr-ready/contracts/v3/ERC8004ReputationRegistryV3.sol`  
**Role**: Reputation  
**Version**: V3 PR-ready

### Summary

Functionally identical to Contract 06 (V3 early). Import path for the identity registry differs (`./ERC8004IdentityRegistryV3.sol` vs relative paths). All audit fixes (HIGH-RR-01, HIGH-RR-02, MEDIUM-RR-01, LOW-RR-01, LOW-RR-03), all state variables, functions, and math are identical. See Contract 06 for the full technical reference.

---

## Contract 10 — ERC8004ValidationRegistryV3 (V3 PR-ready)

**File**: `v3-pr-ready/contracts/v3/ERC8004ValidationRegistryV3.sol`  
**Role**: Validation  
**Version**: V3 PR-ready

### Summary

Functionally identical to Contract 07 (V3 early). Import paths differ. All audit fixes (CRITICAL-VR-01, HIGH-VR-01, HIGH-VR-03, MEDIUM-VR-01, MEDIUM-VR-02, MEDIUM-VR-03), all state variables, functions, and math are identical. See Contract 07 for the full technical reference.

---

## Contract 11 — VaultfireERC8004AdapterV3 (V3 PR-ready)

**File**: `v3-pr-ready/contracts/v3/VaultfireERC8004AdapterV3.sol`  
**Role**: Adapter  
**Version**: V3 PR-ready (uses AIPartnershipBondsV2)

### Summary

The V3 PR-ready Adapter is a major upgrade over V1. It applies all V3 adapter audit fixes: CRITICAL-AD-01 restricts `syncPartnershipReputation` to bond participants only; HIGH-AD-01 removes `block.timestamp` from `claimHash` for determinism; HIGH-AD-02 adds `bondValidationRequested` deduplication; HIGH-AD-03 adds `receive()` and `rescueETH()`; MEDIUM-AD-01 removes the broken no-arg `discoverVaultfireAgents` overload; MEDIUM-AD-02 adds bounds check on quality score; a CEI fix moves `bondReputationSynced = true` before the external call. Imports `AIPartnershipBondsV2`. The V3.3 audit version (Contract 17) uses `AIPartnershipBondsV3` and adds timelocked sync reset and reputation revocation.

### Full Source with Line Numbers (critical sections)

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: import "./ERC8004IdentityRegistryV3.sol";
  4: import "./ERC8004ReputationRegistryV3.sol";
  5: import "./ERC8004ValidationRegistryV3.sol";
  6: import "../AIPartnershipBondsV2.sol";  // V2 bonds — 9-field struct
  7:
  8: contract VaultfireERC8004AdapterV3 {
  9:     AIPartnershipBondsV2 public immutable partnershipBonds;
 10:     ERC8004IdentityRegistryV3 public immutable identityRegistry;
 11:     ERC8004ReputationRegistryV3 public immutable reputationRegistry;
 12:     ERC8004ValidationRegistryV3 public immutable validationRegistry;
 13:
 14:     address public owner;
 15:     event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
 16:     modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
 17:
 18:     mapping(address => bool) public autoRegisteredAgents;
 19:     mapping(uint256 => bool) public bondReputationSynced;
 20:     mapping(uint256 => bool) public bondValidationRequested;  // HIGH-AD-02
 21:
 22:     event AgentAutoRegistered(address indexed, string, uint256);
 23:     event PartnershipReputationSynced(uint256 indexed bondId, address indexed, uint256, uint256);
 24:     event ValidationRequestCreated(uint256 indexed requestId, uint256 indexed bondId,
 25:         address indexed agentAddress, uint256 timestamp);
 26:
 27:     constructor(address _bonds, address _identity, address _reputation, address _validation) {
 28:         // [non-zero checks, immutable assignments, owner = msg.sender]
 29:     }
 30:
 31:     // HIGH-AD-03: receive ETH for forwarding to validationRegistry
 32:     receive() external payable {}
 33:
 34:     // HIGH-AD-03: emergency ETH rescue
 35:     function rescueETH() external onlyOwner {
 36:         uint256 balance = address(this).balance;
 37:         require(balance > 0, "No ETH to rescue");
 38:         (bool success, ) = payable(owner).call{value: balance}("");
 39:         require(success, "ETH rescue failed");
 40:     }
 41:
 42:     // CRITICAL-AD-01: restricted to bond participants
 43:     function syncPartnershipReputation(uint256 bondId) external {
 44:         require(!bondReputationSynced[bondId], "Already synced");
 45:         (, address human, address aiAgent, string memory partnershipType, , , , , bool active)
 46:             = partnershipBonds.bonds(bondId);
 47:         require(active, "Bond not active");
 48:         require(msg.sender == human || msg.sender == aiAgent,
 49:             "Caller is not a bond participant");  // CRITICAL-AD-01
 50:         require(aiAgent != address(0), "Invalid agent address");
 51:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered in ERC-8004");
 52:         uint256 rating = _calculatePartnershipRating(bondId);
 53:         bondReputationSynced[bondId] = true;  // CEI: state BEFORE external call
 54:         reputationRegistry.submitVerifiedFeedback(aiAgent, rating,
 55:             partnershipType, "", bondId);
 56:         emit PartnershipReputationSynced(bondId, aiAgent, rating, block.timestamp);
 57:     }
 58:
 59:     // MEDIUM-AD-02: bounds check
 60:     function _calculatePartnershipRating(uint256 bondId) internal view returns (uint256) {
 61:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 62:         require(qualityScore <= 10000, "Quality score out of range");
 63:         return qualityScore;
 64:     }
 65:
 66:     // HIGH-AD-01: no block.timestamp in claimHash; HIGH-AD-02: deduplication
 67:     function requestPartnershipValidation(uint256 bondId, string calldata claimURI,
 68:             ERC8004ValidationRegistryV3.ValidationType validationType) external payable {
 69:         require(bytes(claimURI).length > 0, "Claim URI required");
 70:         require(!bondValidationRequested[bondId], "Validation already requested for this bond");
 71:         (, , address aiAgent, , , , , , bool active) = partnershipBonds.bonds(bondId);
 72:         require(active, "Bond not active");
 73:         require(aiAgent != address(0), "Invalid agent address");
 74:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered");
 75:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 76:         bytes32 claimHash = keccak256(abi.encodePacked(bondId, qualityScore));  // HIGH-AD-01
 77:         bondValidationRequested[bondId] = true;  // CEI
 78:         uint256 expectedRequestId = validationRegistry.nextRequestId();
 79:         validationRegistry.requestValidation{value: msg.value}(aiAgent, claimURI,
 80:             claimHash, validationType,
 81:             validationType == ERC8004ValidationRegistryV3.ValidationType.MULTI_VALIDATOR ? 3 : 1);
 82:         emit ValidationRequestCreated(expectedRequestId, bondId, aiAgent, block.timestamp);
 83:     }
 84:
 85:     // MEDIUM-AD-01: typed only, no no-arg overload
 86:     function discoverVaultfireAgents(string calldata agentType)
 87:             external pure returns (address[] memory agents) {
 88:         // V3 shim: agentsByCapability removed from IdentityRegistry
 89:         // Off-chain: filter AgentRegistered events by capabilitiesHash
 90:         agentType;
 91:         return new address[](0);
 92:     }
 93:     // [cross-platform view functions — same as V1 but using V3 registry types]
 94: }
```

### Line-by-Line Walkthrough (new/changed sections only)

| Lines | Explanation |
|-------|-------------|
| 32 | `receive() external payable {}` — enables the adapter to accept ETH sent directly to it. Necessary when calling `requestPartnershipValidation` because the adapter forwards ETH to the validation registry via `{value: msg.value}`. Without `receive()`, any ETH mistakenly sent to the adapter would revert. |
| 35–40 | `rescueETH()`: owner can recover any ETH that accumulates in the adapter (e.g., if ETH is accidentally sent without a function call). Uses `.call{}` with CEI pattern. |
| 48–49 | **CRITICAL-AD-01 fix**: `require(msg.sender == human || msg.sender == aiAgent)`. This extracts both participant addresses from the bond struct and verifies the caller is one of them. In V1, no such check existed. |
| 53–54 | **CEI fix**: `bondReputationSynced[bondId] = true` is set BEFORE calling `reputationRegistry.submitVerifiedFeedback(...)`. This prevents a reentrancy attack where a malicious reputation registry implementation could call back into `syncPartnershipReputation` before `bondReputationSynced` is set, bypassing the "already synced" guard. |
| 61–63 | `_calculatePartnershipRating` now includes `require(qualityScore <= 10000)`. Defensive bounds check — the bond contract already enforces this but the adapter checks independently. |
| 70 | `bondValidationRequested[bondId]` deduplication. Once a validation is requested for a bond, no second request can be created. This is a one-way lock — there is no mechanism to reset it in the PR-ready version (timelocked reset added in V3.3 only for the sync, not validation). |
| 76 | `claimHash = keccak256(abi.encodePacked(bondId, qualityScore))` — deterministic, no timestamp. The same bond + same quality score always produces the same hash. |
| 78 | Reading `validationRegistry.nextRequestId()` BEFORE calling `requestValidation`. After the call, the registry increments `nextRequestId`. So `expectedRequestId = nextRequestId` before the call = the ID that will be assigned inside the call. This is an invariant of single-transaction atomicity — no other tx can interleave. |
| 86–92 | `discoverVaultfireAgents(string)` now returns an empty array always. This is a compatibility shim — the ABI is preserved but the V3 identity registry has no on-chain arrays. Off-chain indexers should use `AgentRegistered` events filtered by `capabilitiesHash == keccak256(abi.encodePacked("vaultfire-ai-partnership", agentType))`. |

### State Variables

| Variable | Type | Purpose | Mutability | Who Can Change |
|----------|------|---------|------------|----------------|
| `partnershipBonds` | `AIPartnershipBondsV2` (immutable) | Bond data source | Immutable | Never |
| `identityRegistry` | `ERC8004IdentityRegistryV3` (immutable) | Agent active check | Immutable | Never |
| `reputationRegistry` | `ERC8004ReputationRegistryV3` (immutable) | Reputation writes | Immutable | Never |
| `validationRegistry` | `ERC8004ValidationRegistryV3` (immutable) | Validation requests | Immutable | Never |
| `owner` | `address` | Access control | Mutable | `transferOwnership` |
| `autoRegisteredAgents` | `mapping(address => bool)` | VaultFire partnership flag | Mutable | `registerAgentForPartnership` |
| `bondReputationSynced` | `mapping(uint256 => bool)` | Bond sync deduplication | Mutable | `syncPartnershipReputation` |
| `bondValidationRequested` | `mapping(uint256 => bool)` | Validation deduplication | Mutable | `requestPartnershipValidation` |

### Events

| Event | When fired | Parameters |
|-------|-----------|------------|
| `AgentAutoRegistered` | `registerAgentForPartnership` | agentAddress (indexed), agentType, timestamp |
| `PartnershipReputationSynced` | `syncPartnershipReputation` | bondId (indexed), agentAddress (indexed), rating, timestamp |
| `ValidationRequestCreated` | `requestPartnershipValidation` | requestId (indexed), bondId (indexed), agentAddress (indexed), timestamp |
| `OwnershipTransferred` | `transferOwnership` | previousOwner (indexed), newOwner (indexed) |

### Security Invariants

- Bond participants only can sync reputation (CRITICAL-AD-01)
- CEI on `bondReputationSynced` and `bondValidationRequested` — both set before external calls
- `claimHash` is deterministic — same bondId+qualityScore always produces same hash
- Each bond can only have one validation request (deduplication)
- Quality score must be ≤ 10000 (defensive bounds check)
- ETH rescue available to owner for mistakenly sent ETH
- **V3-PR-ready gap**: `syncPartnershipReputation` can be called unlimited times after a sync reset (no sync count cap) — fixed in V3.3 (HIGH-14) with `MAX_BOND_SYNCS = 3`
- **V3-PR-ready gap**: No mechanism to revoke synced reputation for bad bonds (added in V3.3 HIGH-17)


---

## Contract 12 — ERC8004ReputationRegistryV3 (V3.2, test_fixes)

**File**: `test_fixes/contracts/ERC8004ReputationRegistryV3.sol`  
**Role**: Reputation  
**Version**: V3.2

### Summary

V3.2 adds two changes on top of V3 PR-ready: HIGH-RR-03 (paginated view functions for all feedback arrays) and MEDIUM-RR-03 (authorized callers exempt from rate limiting for high-frequency bond syncs). This version is significant because it resolves a practical deployment problem: when the Adapter needs to sync multiple bonds for the same agent, the V3 rate limiting (1 hour per reviewer-agent pair) would block the second sync within the same hour. V3.2 removes the rate limit check from `submitVerifiedFeedback` and `submitVerifiedFeedbackHashed` for authorized callers. Note: V3.3 (HIGH-09) reverses this decision and reinstates rate limiting for authorized callers using a separate `lastAuthorizedFeedbackTime` mapping.

### Key Additions over V3-early/PR-ready

#### HIGH-RR-03 — Paginated view functions

Six new view functions added to enable safe iteration over unbounded feedback arrays:

```solidity
// Agent feedbacks (plain path)
function getAgentFeedbacksCount(address agentAddress) external view returns (uint256)
function getAgentFeedbacksPaginated(address agentAddress, uint256 offset, uint256 limit)
    external view returns (uint256[] memory page)

// Agent feedbacks (hashed path)
function getAgentFeedbacksHashedCount(address agentAddress) external view returns (uint256)
function getAgentFeedbacksHashedPaginated(address agentAddress, uint256 offset, uint256 limit)
    external view returns (uint256[] memory page)

// Reviewer feedbacks (plain path)
function getReviewerFeedbacksCount(address reviewer) external view returns (uint256)
function getReviewerFeedbacksPaginated(address reviewer, uint256 offset, uint256 limit)
    external view returns (uint256[] memory page)

// Reviewer feedbacks (hashed path)
function getReviewerFeedbacksHashedCount(address reviewer) external view returns (uint256)
function getReviewerFeedbacksHashedPaginated(address reviewer, uint256 offset, uint256 limit)
    external view returns (uint256[] memory page)
```

**Pagination pattern** (identical across all variants):

```solidity
function getAgentFeedbacksPaginated(address agentAddress, uint256 offset, uint256 limit)
        external view returns (uint256[] memory page) {
    uint256[] storage ids = agentFeedbacks[agentAddress];
    uint256 len = ids.length;
    if (offset >= len || limit == 0) return new uint256[](0);  // guard: empty page
    uint256 end = offset + limit > len ? len : offset + limit;  // clamp end
    page = new uint256[](end - offset);
    for (uint256 i = offset; i < end; i++) {
        page[i - offset] = ids[i];
    }
}
```

**Line-by-line analysis of pagination**:
- `offset >= len`: if offset is past the end, return empty — safe against off-by-one
- `limit == 0`: guard against zero-length allocation
- `end = offset + limit > len ? len : offset + limit`: clamping avoids array out-of-bounds on the last page
- `new uint256[](end - offset)`: allocates exactly the right size
- The loop is bounded by `limit` — gas predictable by caller

**Example usage**: agent has 2500 feedbacks. Caller wants page 3 with 100 items per page: `getAgentFeedbacksPaginated(agent, 200, 100)` → returns IDs at indices 200–299.

#### MEDIUM-RR-03 — Authorized callers exempt from rate limiting

In `submitVerifiedFeedback` (V3.2):

```solidity
// [audit-v3.2] MEDIUM-RR-03: Authorized callers exempt from rate limiting
// Authorized callers (e.g., VaultfireERC8004AdapterV3) may need to sync
// multiple bonds for the same agent within one hour. Rate limiting is
// only enforced on the public (unverified) path.
```

The entire `require(block.timestamp >= lastFeedbackTime[...] + FEEDBACK_COOLDOWN)` block is removed from `submitVerifiedFeedback` and `submitVerifiedFeedbackHashed`. The rate limit remains on `submitFeedback` (public) and `submitFeedbackHashed` (public).

**Design rationale**: A single AI agent could be involved in many active partnership bonds. If all bonds complete or are synced in a burst, the adapter needs to call `submitVerifiedFeedback` many times for the same agent. The 1-hour rate limit would block all but the first sync per hour. Since the Adapter is a trusted authorized caller, the rate limit is redundant — it already enforces per-bond deduplication (`bondReputationSynced`).

**V3.3 reversion**: HIGH-09 reinstates rate limiting for authorized callers via a separate `lastAuthorizedFeedbackTime` mapping with `MIN_AUTHORIZED_FEEDBACK_INTERVAL = 1 hours`. The reasoning: a compromised Adapter key could otherwise flood the reputation registry with unlimited writes in a single block.

### Full State Variable Additions

No new state variables added vs V3-PR-ready (the paginated functions are pure view functions that read existing arrays).

### Security Invariants (V3.2 specific)

- Pagination is gas-bounded: `limit` parameter controls maximum items per call
- Empty array returned on `offset >= len` — no revert, safe for integrators
- **V3.2 design decision** (later reversed): authorized callers have no rate limit — a compromised adapter key can write unlimited verified feedback entries (mitigated by V3.3 HIGH-09)

---

## Contract 13 — ERC8004ValidationRegistryV3 (V3.2, test_fixes)

**File**: `test_fixes/contracts/ERC8004ValidationRegistryV3.sol`  
**Role**: Validation  
**Version**: V3.2

### Summary

V3.2 adds two validation-specific changes on top of V3-PR-ready: MEDIUM-VR-04 (hard revert in `submitValidation` for ZK_PROOF type — forces use of `submitValidationZK`) and MEDIUM-VR-05 (escrow returned to requester on DISPUTED outcome). Also inherits the pagination functions from the V3-PR-ready version for validation arrays.

### Key Additions over V3 PR-ready

#### MEDIUM-VR-04 — Hard revert for ZK_PROOF in `submitValidation`

```solidity
// [audit-v3] MEDIUM-VR-04: ZK_PROOF requests must use submitValidationZK
if (request.validationType == ValidationType.ZK_PROOF) {
    revert("Use submitValidationZK for ZK_PROOF type");
}
```

In V3-early/PR-ready, `submitValidation` with `ZK_PROOF` type accepted proof bytes but did NOT call `zkVerifier.verifyProof()`. This means unverified proofs could be stored and counted toward consensus. V3.2 changes this from "require non-empty proof bytes" to a hard revert — ZK_PROOF requests MUST use `submitValidationZK`, which enforces on-chain proof verification. This prevents lazy validators from storing random bytes as "proofs".

#### MEDIUM-VR-05 — Escrow returned to requester on DISPUTED outcome

In `_checkValidationComplete`:

```solidity
} else {
    request.status = ValidationStatus.DISPUTED;
    // MEDIUM-VR-05: Return escrow to requester on dispute
    uint256 escrow = requestEscrow[requestId];
    requestEscrow[requestId] = 0;
    if (escrow > 0) {
        (bool ok,) = payable(request.requester).call{value: escrow}("");
        if (!ok) emit ValidatorSlashed(address(0), escrow, "Disputed escrow return failed",
            block.timestamp);
    }
}
```

In V3-early/PR-ready, DISPUTED requests left the escrow locked in the contract indefinitely — no mechanism to retrieve it. V3.2 returns it immediately when the tie is detected. Note: in V3.2, the DISPUTED branch still does NOT decrement `validatorActiveValidations` for participants (fixed in V3.3 HIGH-15).

### Full State Variable and Function Comparison vs V3-PR-ready

No new state variables. One behavior change in `submitValidation` (hard revert for ZK_PROOF). One behavior change in `_checkValidationComplete` (escrow return on DISPUTED). All other functions identical.

---

## Contract 14 — ERC8004IdentityRegistryV3 (V3.3 audit, vf-audit-contracts)

**File**: `vf-audit-contracts/ERC8004IdentityRegistryV3.sol`  
**Role**: Identity  
**Version**: V3.3 (most complete audit version)

### Summary

This is the V3.3 (vf-audit-contracts) Identity Registry. The only structural difference from V3-early/PR-ready is the removal of the `owner` state variable declaration from within the contract body — in this version, `owner` is inherited from `MissionEnforcement` (which provides `onlyOwner` and ownership management) rather than declared locally. The `forceDeactivate` function still uses `onlyOwner` modifier but delegates ownership management to the base class. All other audit fixes (HIGH-IR-01 through LOW-IR-03) are identical. Import paths are relative to the `vf-audit-contracts/` directory.

### Structural Difference: owner field location

```solidity
// V3-early and V3-PR-ready: owner declared in this contract
address public owner;
modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }

// V3.3 (vf-audit-contracts): owner NOT declared here
// MissionEnforcement base class provides onlyOwner modifier
// No "address public owner" in this file's storage layout
contract ERC8004IdentityRegistryV3 is PrivacyGuarantees, MissionEnforcement {
    // No owner field — inherited from MissionEnforcement
```

This matters for storage layout: in the PR-ready version, `owner` occupies a slot in the IdentityRegistry's own storage at the top. In the V3.3 audit version, `owner` is in the `MissionEnforcement` inherited storage. Any tooling or proxy upgrade system that assumes a fixed storage layout must account for this difference.

All functions, events, and invariants are otherwise identical to Contract 05. See Contract 05 for full technical reference.

---

## Contract 15 — ERC8004ReputationRegistryV3 (V3.3 audit, vf-audit-contracts)

**File**: `vf-audit-contracts/ERC8004ReputationRegistryV3.sol`  
**Role**: Reputation  
**Version**: V3.3 (most complete audit version)

### Summary

The definitive Reputation Registry. Adds four V3.3 fixes on top of V3.2: HIGH-08 (two-step ownership transfer), HIGH-09 (per-authorized-caller rate limiting), HIGH-10 (reviewer must be registered agent — Sybil bypass fix), and HIGH-17 (`revokeVerifiedFeedback`). This is the recommended deployment version.

### Key Additions over V3.2

#### HIGH-08 — Two-step ownership transfer

```solidity
address public pendingOwner;
event OwnershipTransferInitiated(address indexed currentOwner, address indexed pendingOwner);

// Step 1: current owner sets pendingOwner
function transferOwnership(address newOwner) external onlyOwner {
    require(newOwner != address(0), "Invalid new owner");
    pendingOwner = newOwner;
    emit OwnershipTransferInitiated(owner, newOwner);
    // NOTE: owner is NOT changed yet
}

// Step 2: pendingOwner accepts
function acceptOwnership() external {
    require(msg.sender == pendingOwner, "Not pending owner");
    emit OwnershipTransferred(owner, msg.sender);
    owner = msg.sender;
    pendingOwner = address(0);
}
```

**Rationale**: In V3-early/PR-ready, `transferOwnership(newOwner)` immediately sets `owner = newOwner`. If `newOwner` is a typo or an incorrect address, ownership is permanently lost. The two-step pattern requires the new owner to actively confirm acceptance, proving they control the target address. If the target address is wrong, the current owner can call `transferOwnership(correctAddress)` to overwrite `pendingOwner` before acceptance.

**State**: Adds `pendingOwner address` to storage.

#### HIGH-09 — Per-authorized-caller rate limiting (reinstated)

```solidity
mapping(address => mapping(address => uint256)) public lastAuthorizedFeedbackTime;
uint256 public constant MIN_AUTHORIZED_FEEDBACK_INTERVAL = 1 hours;  // = 3600 seconds

// In submitVerifiedFeedback:
require(
    block.timestamp >= lastAuthorizedFeedbackTime[msg.sender][agentAddress]
        + MIN_AUTHORIZED_FEEDBACK_INTERVAL,
    "Authorized rate limit: wait 1 hour between feedbacks for same agent"
);
lastAuthorizedFeedbackTime[msg.sender][agentAddress] = block.timestamp;
```

This reinstates rate limiting for authorized callers (reversed from V3.2 MEDIUM-RR-03). Unlike V3-early which used a single `lastFeedbackTime` mapping for both public and authorized paths, V3.3 uses a SEPARATE mapping `lastAuthorizedFeedbackTime` so the two rate limits are independent. An authorized caller's rate limit does not consume or interfere with a public reviewer's rate limit for the same agent.

**Design tradeoff**: An authorized adapter can still sync at most 1 bond per hour per agent. If multiple bonds complete simultaneously, subsequent syncs within the hour will revert. Integrators should space syncs or use a queue.

#### HIGH-10 — Sybil bypass fix

```solidity
// In submitFeedback and submitFeedbackHashed:
require(
    identityRegistry.isAgentActive(msg.sender),
    "reviewer must be registered"
);
```

**Problem (V3-early through V3.2)**: Any Ethereum address could call `submitFeedback`. An attacker could create unlimited wallets and submit ratings from each to manipulate `averageRating`. The rate limit per (reviewer, agent) pair helps but doesn't prevent Sybil attacks — each new wallet starts fresh.

**Fix**: Reviewer must be an active registered agent in the identity registry. This ties feedback to an ERC-8004 identity — the reviewer must have put effort into registering (URI validation, valid capabilities hash) and bears the same identity constraints as the agent they're reviewing.

**Note**: This check is applied to `submitFeedback` and `submitFeedbackHashed` (the public paths). `submitVerifiedFeedback` and `submitVerifiedFeedbackHashed` are restricted to `onlyAuthorized` callers (the adapter) which are not individual reviewers, so the Sybil check is not needed there.

#### HIGH-17 — revokeVerifiedFeedback

```solidity
// Track which feedbackId was submitted for each bondId
mapping(uint256 => uint256) public bondVerifiedFeedbackId;

// In submitVerifiedFeedback, when bondId != 0:
bondVerifiedFeedbackId[bondId] = feedbackId;

// Revocation function (owner only):
function revokeVerifiedFeedback(uint256 bondId) external onlyOwner {
    require(bondId != 0, "Invalid bondId");
    uint256 feedbackId = bondVerifiedFeedbackId[bondId];
    require(feedbackId != 0, "No verified feedback for this bond");
    Feedback storage fb = feedbacks[feedbackId];
    require(fb.verified, "Feedback already revoked or not verified");
    address agentAddress = fb.agentAddress;
    fb.verified = false;  // Mark as no longer verified (immutable audit trail preserved)
    AgentReputation storage rep = reputations[agentAddress];
    if (rep.verifiedFeedbacks > 0) rep.verifiedFeedbacks -= 1;
    if (rep.totalFeedbacks > 0) rep.totalFeedbacks -= 1;
    rep.lastUpdated = block.timestamp;
    bondVerifiedFeedbackId[bondId] = 0;  // Clear to prevent double-revocation
    emit VerifiedFeedbackRevoked(bondId, feedbackId, agentAddress, block.timestamp);
    emit ReputationUpdated(agentAddress, rep.averageRating, rep.totalFeedbacks, block.timestamp);
}
```

**Design notes**:
- The `Feedback` struct in storage is NOT deleted — the on-chain audit trail is preserved. `fb.verified` is set to `false` in place.
- `averageRating` is NOT recalculated — this would require replaying all feedbacks. The `verifiedFeedbacks` and `totalFeedbacks` counters are decremented, which is the primary trust signal.
- `bondVerifiedFeedbackId[bondId] = 0` prevents double-revocation.
- Only one `feedbackId` per `bondId` is tracked — `submitVerifiedFeedback` overwrites `bondVerifiedFeedbackId[bondId]` if called again for the same bondId (possible if `bondReputationSynced` is reset). The last submitted feedback ID wins.

### Full State Variable Summary (V3.3 additions over V3.2)

| Variable | Type | Added by | Purpose |
|----------|------|----------|---------|
| `pendingOwner` | `address` | HIGH-08 | Pending two-step ownership transfer |
| `lastAuthorizedFeedbackTime` | `mapping(address => mapping(address => uint256))` | HIGH-09 | Rate limit for authorized callers |
| `MIN_AUTHORIZED_FEEDBACK_INTERVAL` | `uint256` constant = 3600 | HIGH-09 | Authorized caller cooldown |
| `bondVerifiedFeedbackId` | `mapping(uint256 => uint256)` | HIGH-17 | Bond → feedbackId for revocation |

### Events (V3.3 additions)

| Event | When fired | Parameters |
|-------|-----------|------------|
| `OwnershipTransferInitiated` | `transferOwnership` | currentOwner (indexed), pendingOwner (indexed) |
| `VerifiedFeedbackRevoked` | `revokeVerifiedFeedback` | bondId (indexed), feedbackId (indexed), agentAddress (indexed), timestamp |

### Security Invariants (V3.3 specific)

- Two-step ownership prevents irrecoverable loss of admin control
- Rate limiting for authorized callers prevents a compromised adapter key from flooding the registry in a single block
- Reviewer must be a registered ERC-8004 agent — Sybil attacks require real identity registration work
- Revocation is idempotent — `bondVerifiedFeedbackId[bondId] = 0` after first revocation prevents double-revocation
- `averageRating` is not recalculated on revocation — accepted limitation noted in contract comments; integrators should use event indexing for full replay

---

## Contract 16 — ERC8004ValidationRegistryV3 (V3.3 audit, vf-audit-contracts)

**File**: `vf-audit-contracts/ERC8004ValidationRegistryV3.sol`  
**Role**: Validation  
**Version**: V3.3 (most complete audit version)

### Summary

The definitive Validation Registry. Adds two V3.3 fixes: HIGH-15 (decrement `validatorActiveValidations` for all validators in DISPUTED requests) and HIGH-19 (decrement for all responders when `cancelRequest` is called). This is the recommended deployment version.

### Key Additions over V3.2

#### HIGH-15 — DISPUTED requests unlock validator active counts

```solidity
} else {
    request.status = ValidationStatus.DISPUTED;

    // [audit-v3.3] HIGH-15: Decrement validatorActiveValidations for all validators
    // who participated in a DISPUTED request. Without this, validators in ties
    // can never withdraw their stake.
    uint256[] memory responses = requestResponses[requestId];
    for (uint256 i = 0; i < responses.length; i++) {
        address validator = validationResponses[responses[i]].validator;
        if (validatorActiveValidations[validator] > 0) {
            validatorActiveValidations[validator] -= 1;
        }
    }

    // MEDIUM-VR-05: Return escrow to requester on dispute
    uint256 escrow = requestEscrow[requestId];
    requestEscrow[requestId] = 0;
    if (escrow > 0) {
        (bool ok,) = payable(request.requester).call{value: escrow}("");
        if (!ok) emit ValidatorSlashed(address(0), escrow, "Disputed escrow return failed",
            block.timestamp);
    }
}
```

**Problem**: In V3-early through V3.2, `_distributeRewards` is only called for APPROVED and REJECTED outcomes. DISPUTED requests (equal approvals and rejections) never call it. Since `_distributeRewards` decrements `validatorActiveValidations`, validators in a tied request would have their active count incremented (in `submitValidation`) but never decremented. They could never satisfy `validatorActiveValidations == 0` in `withdrawValidatorStake`, permanently locking their stake.

**Fix**: When a DISPUTED outcome is detected, iterate all responses and decrement each validator's active count with the safe guard (`if > 0`).

#### HIGH-19 — cancelRequest unlocks validator active counts

```solidity
function cancelRequest(uint256 requestId) external nonReentrant {
    // [existing checks: requester == msg.sender, PENDING status, deadline elapsed]
    
    // [audit-v3.3] HIGH-19: Decrement validatorActiveValidations for validators
    // who submitted responses before this request was cancelled.
    uint256[] memory responses = requestResponses[requestId];
    for (uint256 i = 0; i < responses.length; i++) {
        address validator = validationResponses[responses[i]].validator;
        if (validatorActiveValidations[validator] > 0) {
            validatorActiveValidations[validator] -= 1;
        }
    }
    
    // [escrow return to requester]
}
```

**Problem**: In V3-early through V3.2, if a validation request expires and is cancelled, any validators who had already submitted responses would have their active count permanently incremented. They could not withdraw their stake.

**Fix**: Before returning the escrow, iterate all existing responses and decrement the active count for each validator. This is safe: a cancelled request's responses are final — no further responses can be added (status changes to CANCELLED first, then the loop runs before the ETH transfer).

**Ordering detail**: The status is set to `CANCELLED` before the loop. If the loop's ETH transfer were to reenter, the reentrant call would see `status == CANCELLED` and revert on `require(request.status == ValidationStatus.PENDING)`. The active count decrements have no ETH transfers so there is no reentrancy risk in the loop itself.

### Full Behavioral Summary vs V3.2

| Scenario | V3.2 behavior | V3.3 behavior |
|----------|--------------|---------------|
| DISPUTED outcome | Escrow returned, active counts NOT decremented | Escrow returned, active counts decremented for all participants |
| cancelRequest | Escrow returned, active counts NOT decremented | Escrow returned, active counts decremented for all responders |
| ZK_PROOF in submitValidation | Hard revert (MEDIUM-VR-04) | Same |
| Per-request escrow | Yes (CRITICAL-VR-01) | Same |
| Duplicate response | Prevented (HIGH-VR-01) | Same |

No new state variables. No new events. Two additional loops in `_checkValidationComplete` (DISPUTED branch) and `cancelRequest`.

### Security Invariants (V3.3 specific)

- Validators can ALWAYS eventually withdraw their stake after any terminal state (APPROVED, REJECTED, DISPUTED, CANCELLED)
- The safe decrement guard (`if > 0`) prevents underflow even if called multiple times (idempotent)
- Loop in `cancelRequest` runs BEFORE the ETH transfer — CEI pattern maintained


---

## Contract 17 — `vf-audit-contracts/VaultfireERC8004AdapterV3.sol`

### 1. Header

| Field | Value |
|-------|-------|
| **Filename** | `VaultfireERC8004AdapterV3.sol` |
| **Path** | `vf-audit-contracts/VaultfireERC8004AdapterV3.sol` |
| **Role** | Adapter — V3.3 (AIPartnershipBondsV3, timelocked sync reset HIGH-14, revocation HIGH-17) |
| **Solidity** | `^0.8.25` |
| **License** | MIT |

This is the **final audit-round-3 adapter** connecting VaultFire's AI Partnership Bonds system to the ERC-8004 standard. It inherits every fix from the V3-PR-ready adapter (Contract 11) and adds two critical V3.3 security mechanisms. **HIGH-14** replaces the previously immediate `resetBondReputationSync` with a two-step, 48-hour timelocked process (`initiateBondSyncReset` → `finalizeBondSyncReset`) and caps total re-syncs per bond at `MAX_BOND_SYNCS = 3`, preventing a compromised owner key from silently re-syncing bond reputation an unlimited number of times without governance oversight. **HIGH-17** adds `revokePartnershipReputation(bondId)` — an owner-only function that delegates to `ERC8004ReputationRegistryV3.revokeVerifiedFeedback(bondId)`, enabling reputation correction when bonds exit early or incur a domination penalty. This contract also integrates the 10-field `AIPartnershipBondsV3` bond struct (which adds `distributionCount` at position [9], silently skipped in all tuple destructurings).

---

### 2. Full Source Code

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: 
  4: import "./ERC8004IdentityRegistryV3.sol";
  5: import "./ERC8004ReputationRegistryV3.sol";
  6: import "./ERC8004ValidationRegistryV3.sol";
  7: import "./AIPartnershipBondsV3.sol";
  8: 
  9: /**
 10:  * @title VaultFire ERC-8004 Adapter V3
 11:  * @notice Bridges VaultFire's AI Partnership Bonds with ERC-8004 standard
 12:  * @dev V3 — Applies audit fixes: restrict syncPartnershipReputation, remove timestamp
 13:  *      from claimHash, deduplication guard for validation requests, ETH rescue,
 14:  *      remove broken no-arg discoverVaultfireAgents, bounds-check qualityScore,
 15:  *      CEI fix on bondReputationSynced, emit ValidationRequestCreated with requestId.
 16:  *
 17:  * **V3.3 Changes (2026-03 Audit Round 3):**
 18:  * - HIGH-14: Replace resetBondReputationSync with timelocked, counted re-sync mechanism.
 19:  *            bondSyncCount tracks total syncs per bond; max 3 syncs allowed.
 20:  *            A pending reset requires a 48-hour timelock before it takes effect.
 21:  * - HIGH-17: revokePartnershipReputation() — forwards revocation request to
 22:  *            ReputationRegistry.revokeVerifiedFeedback() for bad/early-exit bonds.
 23:  */
 24: contract VaultfireERC8004AdapterV3 {
 25: 
 26:     AIPartnershipBondsV3 public immutable partnershipBonds;
 27:     ERC8004IdentityRegistryV3 public immutable identityRegistry;
 28:     ERC8004ReputationRegistryV3 public immutable reputationRegistry;
 29:     ERC8004ValidationRegistryV3 public immutable validationRegistry;
 30: 
 31:     address public owner;
 32: 
 33:     event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
 34: 
 35:     modifier onlyOwner() {
 36:         require(msg.sender == owner, "Not owner");
 37:         _;
 38:     }
 39: 
 40:     mapping(address => bool) public autoRegisteredAgents;
 41:     mapping(uint256 => bool) public bondReputationSynced;
 42:     mapping(uint256 => bool) public bondValidationRequested;      // HIGH-AD-02
 43: 
 44:     /// @dev [audit-v3.3] HIGH-14: Total syncs per bond (max MAX_BOND_SYNCS)
 45:     mapping(uint256 => uint256) public bondSyncCount;
 46: 
 47:     uint256 public constant MAX_BOND_SYNCS = 3;
 48: 
 49:     /// @dev [audit-v3.3] HIGH-14: Timestamp when pending reset was initiated
 50:     mapping(uint256 => uint256) public bondSyncResetRequestedAt;
 51: 
 52:     uint256 public constant SYNC_RESET_TIMELOCK = 48 hours;
 53: 
 54:     event AgentAutoRegistered(address indexed agentAddress, string agentType, uint256 timestamp);
 55:     event PartnershipReputationSynced(uint256 indexed bondId, address indexed agentAddress, uint256 rating, uint256 timestamp);
 56:     event ValidationRequestCreated(uint256 indexed requestId, uint256 indexed bondId, address indexed agentAddress, uint256 timestamp);
 57: 
 58:     /// @dev [audit-v3.3] HIGH-14
 59:     event BondSyncResetInitiated(uint256 indexed bondId, address indexed initiator, uint256 availableAt, uint256 timestamp);
 60:     event BondSyncResetFinalized(uint256 indexed bondId, address indexed initiator, uint256 newSyncCount, uint256 timestamp);
 61: 
 62:     /// @dev [audit-v3.3] HIGH-17
 63:     event PartnershipReputationRevoked(uint256 indexed bondId, address indexed agentAddress, uint256 timestamp);
 64: 
 65:     constructor(
 66:         address _partnershipBonds,
 67:         address _identityRegistry,
 68:         address _reputationRegistry,
 69:         address _validationRegistry
 70:     ) {
 71:         require(_partnershipBonds != address(0), "Invalid partnership bonds");
 72:         require(_identityRegistry != address(0), "Invalid identity registry");
 73:         require(_reputationRegistry != address(0), "Invalid reputation registry");
 74:         require(_validationRegistry != address(0), "Invalid validation registry");
 75:         partnershipBonds = AIPartnershipBondsV3(_partnershipBonds);
 76:         identityRegistry = ERC8004IdentityRegistryV3(_identityRegistry);
 77:         reputationRegistry = ERC8004ReputationRegistryV3(_reputationRegistry);
 78:         validationRegistry = ERC8004ValidationRegistryV3(_validationRegistry);
 79:         owner = msg.sender;
 80:     }
 81: 
 82:     function transferOwnership(address newOwner) external onlyOwner {
 83:         require(newOwner != address(0), "Invalid new owner");
 84:         emit OwnershipTransferred(owner, newOwner);
 85:         owner = newOwner;
 86:     }
 87: 
 88:     receive() external payable {}
 89: 
 90:     function rescueETH() external onlyOwner {
 91:         uint256 balance = address(this).balance;
 92:         require(balance > 0, "No ETH to rescue");
 93:         (bool success, ) = payable(owner).call{value: balance}("");
 94:         require(success, "ETH rescue failed");
 95:     }
 96: 
 97:     function registerAgentForPartnership(
 98:         string calldata agentURI,
 99:         string calldata agentType
 100:     ) external {
 101:         require(!autoRegisteredAgents[msg.sender], "Already registered for VaultFire");
 102:         require(bytes(agentURI).length > 0, "Agent URI required");
 103:         require(bytes(agentType).length > 0, "Agent type required");
 104:         if (!identityRegistry.isAgentActive(msg.sender)) {
 105:             revert("Agent must call identityRegistry.registerAgent() first");
 106:         }
 107:         autoRegisteredAgents[msg.sender] = true;
 108:         emit AgentAutoRegistered(msg.sender, agentType, block.timestamp);
 109:     }
 110: 
 111:     function syncPartnershipReputation(uint256 bondId) external {
 112:         require(!bondReputationSynced[bondId], "Already synced");
 113:         require(bondSyncCount[bondId] < MAX_BOND_SYNCS, "Max sync count reached for this bond");
 114:         (
 115:             ,
 116:             address human,
 117:             address aiAgent,
 118:             string memory partnershipType,
 119:             ,,,, bool active,
 120:         ) = partnershipBonds.bonds(bondId);
 120:         require(active, "Bond not active");
 121:         require(msg.sender == human || msg.sender == aiAgent, "Caller is not a bond participant");
 122:         require(aiAgent != address(0), "Invalid agent address");
 123:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered in ERC-8004");
 124:         uint256 rating = _calculatePartnershipRating(bondId);
 125:         bondReputationSynced[bondId] = true;           // CEI — state before external call
 126:         bondSyncCount[bondId] += 1;                    // HIGH-14: increment sync counter
 127:         reputationRegistry.submitVerifiedFeedback(aiAgent, rating, partnershipType, "", bondId);
 128:         emit PartnershipReputationSynced(bondId, aiAgent, rating, block.timestamp);
 129:     }
 130: 
 131:     function revokePartnershipReputation(uint256 bondId) external onlyOwner {
 132:         require(bondId != 0, "Invalid bondId");
 133:         require(bondReputationSynced[bondId], "Bond reputation was never synced");
 134:         (,, address aiAgent,,,,,,,) = partnershipBonds.bonds(bondId);
 135:         require(aiAgent != address(0), "Invalid agent address in bond");
 136:         reputationRegistry.revokeVerifiedFeedback(bondId);
 137:         emit PartnershipReputationRevoked(bondId, aiAgent, block.timestamp);
 138:     }
 139: 
 140:     function initiateBondSyncReset(uint256 bondId) external onlyOwner {
 141:         require(bondId != 0, "Invalid bondId");
 142:         require(bondReputationSynced[bondId], "Bond not yet synced - no reset needed");
 143:         require(bondSyncCount[bondId] < MAX_BOND_SYNCS, "Max sync count reached; reset not permitted");
 144:         require(bondSyncResetRequestedAt[bondId] == 0, "Reset already pending");
 145:         bondSyncResetRequestedAt[bondId] = block.timestamp;
 146:         emit BondSyncResetInitiated(bondId, msg.sender, block.timestamp + SYNC_RESET_TIMELOCK, block.timestamp);
 147:     }
 148: 
 149:     function finalizeBondSyncReset(uint256 bondId) external onlyOwner {
 150:         require(bondId != 0, "Invalid bondId");
 151:         require(bondSyncResetRequestedAt[bondId] != 0, "No pending reset for this bond");
 152:         require(
 153:             block.timestamp >= bondSyncResetRequestedAt[bondId] + SYNC_RESET_TIMELOCK,
 154:             "Timelock not elapsed (48h required)"
 155:         );
 156:         bondSyncResetRequestedAt[bondId] = 0;
 157:         bondReputationSynced[bondId] = false;
 158:         emit BondSyncResetFinalized(bondId, msg.sender, bondSyncCount[bondId], block.timestamp);
 159:     }
 160: 
 161:     function _calculatePartnershipRating(uint256 bondId) internal view returns (uint256 rating) {
 162:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 163:         require(qualityScore <= 10000, "Quality score out of range");
 164:         return qualityScore;
 165:     }
 166: 
 167:     function requestPartnershipValidation(
 168:         uint256 bondId,
 169:         string calldata claimURI,
 170:         ERC8004ValidationRegistryV3.ValidationType validationType
 171:     ) external payable {
 172:         require(bytes(claimURI).length > 0, "Claim URI required");
 173:         require(!bondValidationRequested[bondId], "Validation already requested for this bond");
 174:         (,, address aiAgent,,,,, bool active,) = partnershipBonds.bonds(bondId);
 175:         require(active, "Bond not active");
 176:         require(aiAgent != address(0), "Invalid agent address");
 177:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered");
 178:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
 179:         bytes32 claimHash = keccak256(abi.encodePacked(bondId, qualityScore));
 180:         bondValidationRequested[bondId] = true;
 181:         uint256 expectedRequestId = validationRegistry.nextRequestId();
 182:         validationRegistry.requestValidation{value: msg.value}(
 183:             aiAgent, claimURI, claimHash, validationType,
 184:             validationType == ERC8004ValidationRegistryV3.ValidationType.MULTI_VALIDATOR ? 3 : 1
 185:         );
 186:         emit ValidationRequestCreated(expectedRequestId, bondId, aiAgent, block.timestamp);
 187:     }
 188: 
 189:     function getAgentCrossPlatformReputation(address agentAddress)
 190:         external view returns (
 191:             uint256 vaultfireRating, uint256 erc8004Rating,
 192:             uint256 totalFeedbacks, uint256 verifiedPercentage) {
 193:         require(agentAddress != address(0), "Invalid agent address");
 194:         vaultfireRating = 0;
 195:         (erc8004Rating, totalFeedbacks, , ) = reputationRegistry.getReputation(agentAddress);
 196:         verifiedPercentage = reputationRegistry.getVerifiedFeedbackPercentage(agentAddress);
 197:     }
 198: 
 199:     function isAgentFullyRegistered(address agentAddress)
 200:         external view returns (bool registeredERC8004, bool registeredVaultFire) {
 201:         require(agentAddress != address(0), "Invalid agent address");
 202:         registeredERC8004 = identityRegistry.isAgentActive(agentAddress);
 203:         registeredVaultFire = autoRegisteredAgents[agentAddress];
 204:     }
 205: 
 206:     function discoverVaultfireAgents(string calldata agentType)
 207:         external pure returns (address[] memory agents) {
 208:         agentType;
 209:         return new address[](0);
 210:     }
 211: }
```

---

### 3. Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 1–7 | SPDX + pragma. Four imports: all three V3.3 registry contracts plus `AIPartnershipBondsV3`. Note the V3 bond struct has 10 fields — the final field `distributionCount` is silently skipped in every tuple destructure. |
| 9–23 | NatSpec header. Explicitly enumerates both V3 base fixes and V3.3 additions (HIGH-14, HIGH-17). |
| 24 | Contract declaration — no base class, no `ReentrancyGuard`. No ETH escrow is held by this contract beyond transient forwarding; reentrancy risk is managed via CEI. |
| 26–29 | Four `immutable` registry references. Set once in constructor, zero gas on subsequent reads. |
| 31 | `owner` — mutable address, set to `msg.sender` in constructor. The only privileged role in this contract. |
| 33 | `OwnershipTransferred` event — emitted before the state change in `transferOwnership`. |
| 35–38 | `onlyOwner` modifier — single `require` check, short error string, then `_`. |
| 40 | `autoRegisteredAgents` — tracks agents who completed VaultFire-specific registration (separate from ERC-8004 identity registration). |
| 41 | `bondReputationSynced` — prevents double-sync of the same bond to the reputation registry. |
| 42 | `bondValidationRequested` — deduplication guard (HIGH-AD-02) ensuring each bond creates at most one validation request. |
| 45 | `bondSyncCount` — **HIGH-14 addition**. Counts how many total reputation syncs have been performed for each bond. Lifetime counter, never decremented. |
| 47 | `MAX_BOND_SYNCS = 3` — constant cap. Once a bond has been synced 3 times (original + 2 resets), no further resets are permitted regardless of timelock. |
| 50 | `bondSyncResetRequestedAt` — **HIGH-14 addition**. Stores the `block.timestamp` at which `initiateBondSyncReset` was called. `== 0` means no pending reset. |
| 52 | `SYNC_RESET_TIMELOCK = 48 hours` — compiles to `172800`. The minimum interval between initiating a reset and being able to finalize it. |
| 54–63 | Seven events. Three inherited from V3-PR-ready (`AgentAutoRegistered`, `PartnershipReputationSynced`, `ValidationRequestCreated`). Two new HIGH-14 events (`BondSyncResetInitiated`, `BondSyncResetFinalized`). One new HIGH-17 event (`PartnershipReputationRevoked`). |
| 65–80 | Constructor — four address zero-checks, four immutable assignments, `owner = msg.sender`. |
| 82–86 | `transferOwnership` — emits event BEFORE writing `owner` (correct event ordering). Zero address check prevents bricking. |
| 88 | `receive()` — HIGH-AD-03. Allows the adapter to accept ETH forwarded by callers for validation escrow before passing it to the validation registry. |
| 90–95 | `rescueETH()` — HIGH-AD-03. Emergency drain to `owner`. Uses `.call{value}` not `.transfer`. Reverts if balance is zero or transfer fails. |
| 97–109 | `registerAgentForPartnership` — two string length checks, one ERC-8004 active check, sets `autoRegisteredAgents[msg.sender] = true`, emits `AgentAutoRegistered`. No ETH. |
| 111–129 | `syncPartnershipReputation` — the primary reputation bridge. See Functions section for full detail. |
| 131–138 | `revokePartnershipReputation` — HIGH-17. Owner-only reputation revocation. Delegates to `reputationRegistry.revokeVerifiedFeedback(bondId)`. |
| 140–147 | `initiateBondSyncReset` — Step 1 of the timelocked reset. Sets `bondSyncResetRequestedAt[bondId] = block.timestamp`. |
| 149–159 | `finalizeBondSyncReset` — Step 2. Checks 48 h elapsed, clears the pending flag, clears `bondReputationSynced`. Does NOT reset `bondSyncCount`. |
| 161–165 | `_calculatePartnershipRating` — pure quality score passthrough with bounds check. |
| 167–187 | `requestPartnershipValidation` — HIGH-AD-01/02 validation request with deterministic `claimHash` and deduplication. |
| 189–197 | `getAgentCrossPlatformReputation` — view aggregation of VaultFire (placeholder 0) + ERC-8004 reputation. |
| 199–204 | `isAgentFullyRegistered` — dual registration check. |
| 206–210 | `discoverVaultfireAgents(string)` — MEDIUM-AD-01 shim. Always returns empty array. |

---

### 4. State Variables

| Name | Type | Mutability | Who Can Change | Purpose |
|------|------|-----------|----------------|---------|
| `partnershipBonds` | `AIPartnershipBondsV3 immutable` | Immutable | Constructor only | Bond data source |
| `identityRegistry` | `ERC8004IdentityRegistryV3 immutable` | Immutable | Constructor only | Agent active check |
| `reputationRegistry` | `ERC8004ReputationRegistryV3 immutable` | Immutable | Constructor only | Reputation write/revoke target |
| `validationRegistry` | `ERC8004ValidationRegistryV3 immutable` | Immutable | Constructor only | Validation request target |
| `owner` | `address` | Mutable | `transferOwnership` (current owner) | Privileged role for HIGH-14 and HIGH-17 functions |
| `autoRegisteredAgents` | `mapping(address => bool)` | Mutable | `registerAgentForPartnership` (self) | VaultFire-specific registration flag |
| `bondReputationSynced` | `mapping(uint256 => bool)` | Mutable | `syncPartnershipReputation` (sets true); `finalizeBondSyncReset` (sets false) | Prevents double-sync; controls whether re-sync is possible |
| `bondValidationRequested` | `mapping(uint256 => bool)` | Mutable | `requestPartnershipValidation` (sets true) | Deduplication — one validation per bond |
| `bondSyncCount` | `mapping(uint256 => uint256)` | Mutable | `syncPartnershipReputation` (increments) | Lifetime sync counter, never decremented, enforces MAX_BOND_SYNCS cap |
| `MAX_BOND_SYNCS` | `uint256 constant = 3` | Constant | Never | Maximum allowed reputation syncs per bond |
| `bondSyncResetRequestedAt` | `mapping(uint256 => uint256)` | Mutable | `initiateBondSyncReset` (sets timestamp); `finalizeBondSyncReset` (sets 0) | Timelock anchor for pending reset |
| `SYNC_RESET_TIMELOCK` | `uint256 constant = 48 hours` | Constant | Never | Minimum delay between initiate and finalize reset (172800 seconds) |

---

### 5. Functions

#### `constructor`

| Attribute | Value |
|-----------|-------|
| Signature | `constructor(address, address, address, address)` |
| Visibility | `public` (implicit) |
| Modifiers | None |
| State changes | Sets all four immutables, sets `owner` |
| Reverts | Any zero address |

#### `transferOwnership(address newOwner)`

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| State changes | `owner = newOwner` |
| Events | `OwnershipTransferred(old, new)` |
| Reverts | `newOwner == address(0)` |

#### `receive()`

| Attribute | Value |
|-----------|-------|
| Visibility | `external payable` |
| Behavior | Accepts ETH with no logic — necessary so callers can fund the adapter before it forwards ETH to `validationRegistry.requestValidation` |
| Reverts | Never |

#### `rescueETH()`

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Behavior | Drains entire ETH balance to `owner` via `.call{value: balance}` |
| Reverts | Balance == 0; `.call` failure |

#### `registerAgentForPartnership(string agentURI, string agentType)`

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | None |
| Preconditions | Not already registered; non-empty URI and type; `identityRegistry.isAgentActive(msg.sender)` |
| State changes | `autoRegisteredAgents[msg.sender] = true` |
| Events | `AgentAutoRegistered` |
| Reverts | Already registered; empty strings; agent not ERC-8004 active |

#### `syncPartnershipReputation(uint256 bondId)`

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | None (caller-restricted by bond participant check) |
| Preconditions | Not yet synced; `bondSyncCount < 3`; bond active; caller is human or aiAgent; aiAgent ERC-8004 active |
| State changes | `bondReputationSynced[bondId] = true` (before external call); `bondSyncCount[bondId] += 1` (before external call) |
| External calls | `reputationRegistry.submitVerifiedFeedback(...)` |
| Events | `PartnershipReputationSynced` |
| Reverts | Already synced; sync cap reached; bond inactive; caller not participant; invalid aiAgent; agent not ERC-8004 registered |
| CEI | Both state writes occur before the external call |

#### `revokePartnershipReputation(uint256 bondId)` — HIGH-17

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Preconditions | `bondId != 0`; bond was previously synced |
| State changes | None in this contract (reputation registry updates its own state) |
| External calls | `reputationRegistry.revokeVerifiedFeedback(bondId)` |
| Events | `PartnershipReputationRevoked(bondId, aiAgent, timestamp)` |
| Reverts | Not owner; invalid bondId; never synced; aiAgent address(0) |
| Note | Does not set `bondReputationSynced[bondId] = false` — the bond remains marked as synced even after revocation. A separate `initiateBondSyncReset` call is needed if re-sync is desired. |

#### `initiateBondSyncReset(uint256 bondId)` — HIGH-14 Step 1

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Preconditions | `bondId != 0`; bond is synced (`bondReputationSynced[bondId]`); `bondSyncCount < MAX_BOND_SYNCS`; no pending reset (`bondSyncResetRequestedAt == 0`) |
| State changes | `bondSyncResetRequestedAt[bondId] = block.timestamp` |
| Events | `BondSyncResetInitiated(bondId, msg.sender, block.timestamp + 172800, block.timestamp)` |
| Reverts | Not owner; invalid bondId; not synced; sync cap reached; reset already pending |

#### `finalizeBondSyncReset(uint256 bondId)` — HIGH-14 Step 2

| Attribute | Value |
|-----------|-------|
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| Preconditions | Pending reset exists; `block.timestamp >= bondSyncResetRequestedAt[bondId] + 172800` |
| State changes | `bondSyncResetRequestedAt[bondId] = 0`; `bondReputationSynced[bondId] = false` |
| Events | `BondSyncResetFinalized(bondId, msg.sender, bondSyncCount[bondId], block.timestamp)` |
| Reverts | Not owner; no pending reset; timelock not elapsed |
| Note | `bondSyncCount` is NOT reset. After finalization, `syncPartnershipReputation` can be called again — but only if `bondSyncCount` is still below 3. |

#### `_calculatePartnershipRating(uint256 bondId)` — internal view

| Attribute | Value |
|-----------|-------|
| Visibility | `internal view` |
| Behavior | Reads `partnershipBonds.partnershipQualityScore(bondId)`, requires `<= 10000`, returns as-is |
| Reverts | Quality score > 10000 |

#### `requestPartnershipValidation(uint256 bondId, string claimURI, ValidationType)`

| Attribute | Value |
|-----------|-------|
| Visibility | `external payable` |
| Preconditions | Non-empty claimURI; not already requested; bond active; aiAgent non-zero and ERC-8004 active |
| State changes | `bondValidationRequested[bondId] = true` (before external call, CEI) |
| External calls | `validationRegistry.requestValidation{value: msg.value}(...)` |
| Events | `ValidationRequestCreated(expectedRequestId, bondId, aiAgent, timestamp)` |
| Note on requestId | Read `validationRegistry.nextRequestId()` before the call; the actual ID is that value, since the call increments it |

#### `getAgentCrossPlatformReputation(address)` — view

Returns `(vaultfireRating=0, erc8004Rating, totalFeedbacks, verifiedPercentage)`. VaultFire rating is a placeholder pending `AIPartnershipBonds` exposing an aggregated per-agent score.

#### `isAgentFullyRegistered(address)` — view

Returns `(registeredERC8004, registeredVaultFire)`. Two independent boolean checks.

#### `discoverVaultfireAgents(string)` — pure

Always returns an empty `address[]`. ABI-compatibility shim; callers should query indexed `AgentRegistered` events from the identity registry.

---

### 6. Math

#### Sync Count Cap

```
bondSyncCount[bondId] starts at 0.
Each call to syncPartnershipReputation increments it by 1.
Condition: bondSyncCount[bondId] < MAX_BOND_SYNCS (= 3)
→ Valid sync calls: when count is 0, 1, or 2 (three total syncs allowed)
→ After third sync: bondSyncCount[bondId] = 3; all future syncs revert
→ initiateBondSyncReset also checks count < 3; reset is not available once the cap is reached
```

#### Timelock Arithmetic

```
initiate at T₀: bondSyncResetRequestedAt[bondId] = T₀
finalize condition: block.timestamp >= T₀ + 172800
Earliest finalize: T₀ + 172800 seconds = T₀ + 48h

Example: initiate at Unix 1748000000
Earliest finalize: 1748000000 + 172800 = 1748172800
(≈ 48 hours later)
```

#### Claim Hash (deterministic)

```
claimHash = keccak256(abi.encodePacked(bondId, qualityScore))

bondId = 42, qualityScore = 8500
→ abi.encodePacked(uint256(42), uint256(8500))
→ keccak256 of that 64-byte sequence
→ Fixed 32-byte result regardless of when the call happens
```

No `block.timestamp` in the hash — HIGH-AD-01 fix prevents replay with different timing from producing a distinct hash.

---

### 7. Events

| Event | When Fired | Indexed Fields | Data Fields |
|-------|-----------|----------------|-------------|
| `AgentAutoRegistered` | `registerAgentForPartnership` success | `agentAddress` | `agentType`, `timestamp` |
| `PartnershipReputationSynced` | `syncPartnershipReputation` success | `bondId`, `agentAddress` | `rating`, `timestamp` |
| `ValidationRequestCreated` | `requestPartnershipValidation` success | `requestId`, `bondId`, `agentAddress` | `timestamp` |
| `BondSyncResetInitiated` | `initiateBondSyncReset` success | `bondId`, `initiator` | `availableAt` (when finalize becomes possible), `timestamp` |
| `BondSyncResetFinalized` | `finalizeBondSyncReset` success | `bondId`, `initiator` | `newSyncCount` (current count, not reset), `timestamp` |
| `PartnershipReputationRevoked` | `revokePartnershipReputation` success | `bondId`, `agentAddress` | `timestamp` |
| `OwnershipTransferred` | `transferOwnership` success | `previousOwner`, `newOwner` | — |

---

### 8. Security Invariants

- **Sync cap is permanent.** Once `bondSyncCount[bondId] == 3`, no path resets it to zero. Re-sync is permanently unavailable for that bond. This prevents adversarial owners from repeatedly re-syncing a manipulated bond reputation.
- **Timelock cannot be bypassed.** The only path from `bondReputationSynced[bondId] = false` runs through `finalizeBondSyncReset`, which requires the 48-hour timelock elapsed AND `bondSyncCount < MAX_BOND_SYNCS`. A single instant reset (as in Contract 20) does not exist.
- **Revocation does not re-enable syncing.** `revokePartnershipReputation` does not set `bondReputationSynced[bondId] = false`; a separate reset initiation is required.
- **No ETH stranded.** The adapter holds no ETH intentionally; `rescueETH` provides an emergency drain path.
- **No arbitrary caller can sync.** `syncPartnershipReputation` enforces `msg.sender == human || msg.sender == aiAgent` — CRITICAL-AD-01 fix prevents third-party reputation injection.
- **CEI everywhere.** All state mutations in `syncPartnershipReputation` and `requestPartnershipValidation` execute before any external call.
- **Revocation requires prior sync.** `require(bondReputationSynced[bondId])` in `revokePartnershipReputation` prevents double-revocation of never-synced bonds.

---

### 9. ERC-8004 Standard Compliance

| ERC-8004 Section | Implemented By |
|-----------------|----------------|
| §3 — Interoperability | Entire adapter contract |
| §4 — Agent reputation portability | `syncPartnershipReputation`, `revokePartnershipReputation` |
| §5 — Validation integration | `requestPartnershipValidation` |
| §6 — Privacy | No personal data stored; only on-chain bond IDs |
| §7 — Agent discovery | `discoverVaultfireAgents` (shim) |

---

### 10. How the Registries Fit Together

The adapter is the **bridge layer** in the system. It does not store canonical reputation or identity data; those live in the registries. The flow for a completed bond:

```
AIPartnershipBondsV3 (off-ERC8004)
        │  bond.active == true
        │  bond.human, bond.aiAgent, bond.partnershipType
        │  partnershipQualityScore(bondId)
        ▼
VaultfireERC8004AdapterV3.syncPartnershipReputation(bondId)
   ├── checks ERC8004IdentityRegistryV3.isAgentActive(aiAgent)
   ├── writes bondReputationSynced + bondSyncCount (CEI)
   └── calls ERC8004ReputationRegistryV3.submitVerifiedFeedback(...)
              └── updates agent's on-chain ERC-8004 reputation
```

The HIGH-14 timelock interacts with the same bridge:

```
Owner calls initiateBondSyncReset(bondId)
   └── writes bondSyncResetRequestedAt[bondId] = block.timestamp
       (no registry contact)

48h elapses

Owner calls finalizeBondSyncReset(bondId)
   └── clears bondReputationSynced[bondId] = false
       (no registry contact — next syncPartnershipReputation will update registry)
```

HIGH-17 revocation routes to the reputation registry directly:

```
Owner calls revokePartnershipReputation(bondId)
   └── calls ERC8004ReputationRegistryV3.revokeVerifiedFeedback(bondId)
              └── decrements verifiedFeedbacks + totalFeedbacks for the agent
```

---

### 11. Dependencies and Rebuild Recipe

| Attribute | Value |
|-----------|-------|
| Compiler | `solc ^0.8.25` |
| Optimizer | 200 runs |
| External imports | None (no OpenZeppelin in this file) |
| Internal imports | `ERC8004IdentityRegistryV3`, `ERC8004ReputationRegistryV3`, `ERC8004ValidationRegistryV3`, `AIPartnershipBondsV3` |
| Constructor args | `(address _partnershipBonds, address _identityRegistry, address _reputationRegistry, address _validationRegistry)` |
| Deploy after | All four imported contracts |
| Post-deploy | `reputationRegistry.setAuthorizedCaller(adapter, true)` |
| V3.3 vs V3-decoded difference | This contract uses `initiateBondSyncReset` + `finalizeBondSyncReset` (timelocked HIGH-14); Contract 20 uses immediate `resetBondReputationSync` |


---

## Contract 18 — `vf-audit-contracts/IERC8004IdentityRegistry.sol`

### 1. Header

| Field | Value |
|-------|-------|
| **Filename** | `IERC8004IdentityRegistry.sol` |
| **Path** | `vf-audit-contracts/IERC8004IdentityRegistry.sol` |
| **Role** | Interface — minimal ERC-8004 identity check surface for bond contracts |
| **Solidity** | `^0.8.25` |
| **License** | MIT |

This 13-line `interface` is the thinnest possible contract in the entire ERC-8004 suite. Its sole purpose is to give `AIPartnershipBondsV3` (and any future bond or protocol contract) a typed, compile-time-checked way to call `isAgentActive` on whatever identity registry is deployed, without importing the full `ERC8004IdentityRegistryV3` implementation. The interface was introduced in **audit round 3 as HIGH-16**: before this fix, bond contracts had no mechanism to verify that an `aiAgent` address was ERC-8004 registered at bond creation time. A bond could be created with an arbitrary or unregistered agent address, and subsequent sync calls to the adapter would fail silently or inconsistently. By referencing `IERC8004IdentityRegistry` at bond creation, the check happens at the earliest possible point in the workflow.

---

### 2. Full Source Code

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: 
  4: /**
  5:  * @title IERC8004IdentityRegistry
  6:  * @notice Minimal interface for bond contracts to verify agent ERC-8004 registration.
  7:  * @dev [audit-v3.3] HIGH-16: Bond creation should verify that the AI agent is
  8:  *      ERC-8004 registered before allowing a bond to be created.
  9:  */
 10: interface IERC8004IdentityRegistry {
 11:     /// @notice Returns true if the agent is registered and active.
 12:     function isAgentActive(address agentAddress) external view returns (bool);
 13: }
```

---

### 3. Line-by-Line Walkthrough

| Line | Explanation |
|------|-------------|
| 1 | SPDX license identifier — MIT |
| 2 | Pragma: requires compiler ≥ 0.8.25. No upper bound. Matches all V3.3 contracts. |
| 4–9 | NatSpec. `@title` names the interface. `@notice` explains usage context. `@dev` cites HIGH-16 audit finding — the design rationale lives in the code, not a separate doc. |
| 10 | `interface IERC8004IdentityRegistry` — no base interface. Interfaces in Solidity 0.8 may only have `external` functions and no state variables. |
| 11 | NatSpec for the single function — brief one-line description. |
| 12 | `function isAgentActive(address agentAddress) external view returns (bool)` — the entire callable surface of this interface. `external` (required for interface functions). `view` (no state changes, safe to call from bond creation which may itself be non-payable). Returns a single `bool`. |
| 13 | Closing brace. |

---

### 4. State Variables

None. Interfaces cannot declare state variables in Solidity.

---

### 5. Functions

#### `isAgentActive(address agentAddress)` — external view returns (bool)

| Attribute | Value |
|-----------|-------|
| Signature | `isAgentActive(address)` |
| Selector | `keccak256("isAgentActive(address)")[0:4]` |
| Visibility | `external` |
| Mutability | `view` |
| Returns | `true` if agent is registered and `agentStatus == AgentStatus.ACTIVE` in the concrete implementation; `false` otherwise |
| Implementors | `ERC8004IdentityRegistryV3` (in all three V3.3 registry contracts) |
| Callers (intended) | `AIPartnershipBondsV3.createBond` (HIGH-16 check), `VaultfireERC8004AdapterV3.syncPartnershipReputation`, `VaultfireERC8004AdapterV3.registerAgentForPartnership` |

The function selector is the same as the one used in `ERC8004IdentityRegistryV3`. Any contract that imports `IERC8004IdentityRegistry` and receives an address of the live registry will dispatch correctly because the selector matches.

---

### 6. Math

No computations. The interface is purely a type declaration.

---

### 7. Events

None declared. Interfaces may declare events (as of Solidity 0.8.22), but this interface does not.

---

### 8. Security Invariants

- **No callable bytecode.** An `interface` in Solidity has no runtime bytecode. It cannot be deployed directly. Attempting to call `new IERC8004IdentityRegistry()` is a compile error.
- **No false positive through inheritance.** Because this is an interface (not an abstract contract), a contract claiming to implement it must provide `isAgentActive` with the exact signature. The Solidity compiler enforces this at compile time.
- **HIGH-16 enforcement point.** By importing this interface into `AIPartnershipBondsV3`, the bond contract gains the ability to call `isAgentActive` before committing bond state, ensuring only ERC-8004-registered agents participate in bonds from the moment of creation.

---

### 9. ERC-8004 Standard Compliance

| ERC-8004 Section | Implemented By |
|-----------------|----------------|
| §2 — Agent identity attestation surface | This interface exposes the canonical `isAgentActive` check referenced throughout §2 of the standard |
| §8 — Interoperability extension points | Defines the minimal interface third-party protocols must implement to interact with ERC-8004 identity |

---

### 10. How the Registries Fit Together

`IERC8004IdentityRegistry` sits at the outer boundary of the ERC-8004 system. Contracts in the VaultFire domain (bonds, adapters, external protocols) only need to know one thing about the identity registry: whether an agent is active. This interface is the **contractual minimum** they need to import.

```
AIPartnershipBondsV3
    │  imports IERC8004IdentityRegistry
    │  constructor receives address identityRegistry_
    │  at bond creation:
    │      IERC8004IdentityRegistry(identityRegistry_).isAgentActive(aiAgent)
    │      → dispatches to ERC8004IdentityRegistryV3 (the deployed implementation)
    └── reverts if agent not active (HIGH-16 fix)
```

The full `ERC8004IdentityRegistryV3` is not imported by bond contracts — only this interface. This reduces coupling and prevents bond contracts from calling administrative functions (e.g., `registerAgent`, `deactivateAgent`) that they have no business calling.

---

### 11. Dependencies and Rebuild Recipe

| Attribute | Value |
|-----------|-------|
| Compiler | `solc ^0.8.25` |
| Optimizer | Any (zero bytecode produced) |
| External deps | None |
| Internal deps | None |
| Deploy | Not deployable — interface only |
| Usage | `import "./IERC8004IdentityRegistry.sol"; IERC8004IdentityRegistry(addr).isAgentActive(agent)` |


---

## Contract 19 — `theloopbreaker-site/contracts/ERC8004ValidationRegistry.sol`

### 1. Header

| Field | Value |
|-------|-------|
| **Filename** | `ERC8004ValidationRegistry.sol` |
| **Path** | `theloopbreaker-site/contracts/ERC8004ValidationRegistry.sol` |
| **Role** | Validation — V1 theloopbreaker variant with unpatched underflow in `_distributeRewards` |
| **Solidity** | `^0.8.25` |
| **License** | MIT |

This contract is a near-copy of the canonical `contracts/ERC8004ValidationRegistry.sol` (Contract 3) deployed on the theloopbreaker site. It shares the same struct layout, events, most function signatures, and paginated view functions. It diverges in four material ways from the canonical V1 and all V3 variants. First, `submitValidation` accepts `payable` (allowing validators to top up stake inline) rather than using the V3 pre-staking model. Second, `submitValidationZK` does not bind `publicInputs[0]` to `claimHash` — the ZK proof is verified but not anchored to the specific claim being validated, meaning a valid proof for claim A could be submitted for claim B. Third, `_distributeRewards` uses **raw subtraction** for the slash operation (`validatorStakes[validator] -= VALIDATION_REWARD`) **without the `min()` underflow guard** present in the canonical V1 — this is a live arithmetic underflow bug: if a validator's stake has been reduced below `VALIDATION_REWARD` (e.g., via partial withdrawal followed by two concurrent slashes), the subtraction reverts in Solidity ≥ 0.8 (checked arithmetic), permanently bricking the reward distribution for that request. Fourth, the `ValidationStatus` enum has only four values (no `CANCELLED`), so `cancelRequest` is absent.

---

### 2. Full Source Code

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: 
  4: import "./PrivacyGuarantees.sol";
  5: import "./ERC8004IdentityRegistry.sol";
  6: import "./BeliefAttestationVerifier.sol";
  7: import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
  8: 
  9: /**
 10:  * @title ERC-8004 Validation Registry for VaultFire
 11:  * @notice Cryptoeconomic validation mechanisms for agent claims
 12:  * @dev Implements ERC-8004 validation with ZK proofs and economic stakes
 13:  */
 14: /// @custom:audit-fix HIGH-001 — Added ReentrancyGuard; replaced .transfer() with .call{} (2026-02-23)
 15: contract ERC8004ValidationRegistry is PrivacyGuarantees, ReentrancyGuard {
 16: 
 17:     ERC8004IdentityRegistry public immutable identityRegistry;
 18:     BeliefAttestationVerifier public immutable zkVerifier;
 19: 
 20:     enum ValidationType {
 21:         STAKER_RERUN,
 22:         ZK_PROOF,
 23:         TEE_ORACLE,
 24:         TRUSTED_JUDGE,
 25:         MULTI_VALIDATOR
 26:     }
 27: 
 28:     enum ValidationStatus {
 29:         PENDING,
 30:         APPROVED,
 31:         REJECTED,
 32:         DISPUTED
 33:     }
 34:     // ⚠ No CANCELLED value — cancelRequest function is absent
 35: 
 36:     struct ValidationRequest {
 37:         uint256 requestId;
 38:         address agentAddress;
 39:         address requester;
 40:         string claimURI;
 41:         bytes32 claimHash;
 42:         ValidationType validationType;
 43:         uint256 stakeAmount;
 44:         uint256 createdAt;
 45:         ValidationStatus status;
 46:         uint256 validatorsRequired;
 47:         uint256 approvalsCount;
 48:         uint256 rejectionsCount;
 49:     }
 50: 
 51:     struct ValidationResponse {
 52:         uint256 responseId;
 53:         uint256 requestId;
 54:         address validator;
 55:         bool approved;
 56:         string evidenceURI;
 57:         bytes zkProof;
 58:         uint256 timestamp;
 59:         uint256 validatorStake;
 60:     }
 61: 
 62:     mapping(uint256 => ValidationRequest) public validationRequests;
 63:     uint256 public nextRequestId = 1;
 64:     mapping(uint256 => uint256[]) public requestResponses;
 65:     mapping(uint256 => ValidationResponse) public validationResponses;
 65:     uint256 public nextResponseId = 1;
 66:     mapping(address => uint256) public validatorStakes;
 67:     mapping(address => uint256) public validatorActiveValidations;
 68:     mapping(address => uint256[]) public agentValidationRequests;
 69: 
 70:     uint256 public constant MIN_VALIDATOR_STAKE = 1 ether;
 71:     uint256 public constant VALIDATION_REWARD = 0.1 ether;
 72: 
 73:     event ValidationRequested(...);       // same signature as Contract 3
 74:     event ValidationResponseSubmitted(...);
 75:     event ValidationCompleted(...);
 76:     event ValidatorStaked(...);
 77:     event ValidatorSlashed(...);
 78:     event ValidatorStakeWithdrawn(...);
 79: 
 80:     constructor(address _identityRegistry, address _zkVerifier) {
 81:         require(_identityRegistry != address(0), "Invalid identity registry");
 82:         require(_zkVerifier != address(0), "Invalid ZK verifier");
 83:         identityRegistry = ERC8004IdentityRegistry(_identityRegistry);
 84:         zkVerifier = BeliefAttestationVerifier(_zkVerifier);
 85:     }
 86: 
 87:     function requestValidation(
 88:         address agentAddress,
 89:         string calldata claimURI,
 90:         bytes32 claimHash,
 91:         ValidationType validationType,
 92:         uint256 validatorsRequired
 93:     ) external payable {
 94:         // ... identical logic to Contract 3 ...
 95:         // stakeAmount = msg.value; stored in ValidationRequest
 96:     }
 97: 
 98:     function submitValidation(
 99:         uint256 requestId,
 100:         bool approved,
 101:         string calldata evidenceURI,
 102:         bytes calldata zkProof
 103:     ) external payable nonReentrant {           // ← payable: validators may stake inline
 104:         ValidationRequest storage request = validationRequests[requestId];
 105:         require(request.status == ValidationStatus.PENDING, "Request not pending");
 106:         require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE, "Insufficient validator stake");
 107:         if (request.validationType == ValidationType.ZK_PROOF) {
 108:             require(zkProof.length > 0, "ZK proof required");
 109:         }
 110:         // ... response creation, count updates, _checkValidationComplete ...
 111:     }
 112: 
 113:     function submitValidationZK(
 114:         uint256 requestId,
 115:         bool approved,
 116:         string calldata evidenceURI,
 117:         bytes calldata proofBytes,
 118:         uint256[] calldata publicInputs
 119:     ) external payable nonReentrant {
 120:         ValidationRequest storage request = validationRequests[requestId];
 121:         require(request.status == ValidationStatus.PENDING, "Request not pending");
 122:         require(request.validationType == ValidationType.ZK_PROOF, "Not a ZK validation request");
 123:         require(validatorStakes[msg.sender] >= MIN_VALIDATOR_STAKE, "Insufficient validator stake");
 124:         require(proofBytes.length > 0, "ZK proof required");
 125:         bool ok = zkVerifier.verifyProof(proofBytes, publicInputs);
 126:         require(ok, "ZK proof invalid");
 126:         // ⚠ Does NOT check publicInputs[0] == uint256(request.claimHash)
 127:         // ... response creation identical to submitValidation ...
 128:     }
 129: 
 130:     function _checkValidationComplete(uint256 requestId) internal {
 131:         // ... majority logic, calls _distributeRewards or sets DISPUTED ...
 132:         // No CANCELLED status — no cancelRequest function
 133:     }
 134: 
 135:     // slither-disable-next-line reentrancy-eth
 136:     function _distributeRewards(uint256 requestId, bool approved) internal {
 137:         ValidationRequest storage request = validationRequests[requestId];
 138:         uint256[] memory responses = requestResponses[requestId];
 139:         for (uint256 i = 0; i < responses.length; i++) {
 140:             ValidationResponse storage response = validationResponses[responses[i]];
 141:             address validator = response.validator;
 142:             if (response.approved == approved) {
 143:                 validatorActiveValidations[validator] -= 1;   // CEI: before .call
 144:                 (bool rewardSuccess, ) = payable(validator).call{value: VALIDATION_REWARD}("");
 145:                 if (!rewardSuccess) {
 146:                     emit ValidatorSlashed(validator, 0, "Reward transfer failed - skipped", block.timestamp);
 147:                 }
 148:             } else {
 149:                 // ⚠ UNDERFLOW BUG: raw subtraction without min() guard
 150:                 validatorStakes[validator] -= VALIDATION_REWARD;   // can revert if stake < 0.1 ether
 151:                 validatorActiveValidations[validator] -= 1;
 152:                 emit ValidatorSlashed(validator, VALIDATION_REWARD, "Voted against majority", block.timestamp);
 153:             }
 154:         }
 155:     }
 156: 
 157:     function stakeAsValidator() external payable nonReentrant { /* identical to Contract 3 */ }
 158: 
 158:     function withdrawValidatorStake(uint256 amount) external nonReentrant { /* identical to Contract 3 */ }
 159: 
 160:     function getValidationRequest(uint256 requestId) external view returns (...) { /* identical */ }
 161: 
 162:     function getValidationResponses(uint256 requestId) external view returns (uint256[] memory) { /* identical */ }
 163: 
 164:     // Paginated view functions (same as Contract 3):
 165:     function getValidationResponsesCount(uint256 requestId) external view returns (uint256) { /* identical */ }
 166:     function getValidationResponsesPaginated(uint256, uint256, uint256) external view returns (uint256[] memory) { /* identical */ }
 167:     function getAgentValidationRequestsCount(address) external view returns (uint256) { /* identical */ }
 168:     function getAgentValidationRequestsPaginated(address, uint256, uint256) external view returns (uint256[] memory) { /* identical */ }
 169: }
```

---

### 3. Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 1–7 | SPDX + pragma + imports. Identical to Contract 3: `PrivacyGuarantees`, V1 `ERC8004IdentityRegistry` (not V3), `BeliefAttestationVerifier`, `ReentrancyGuard`. Note: uses V1 identity registry, not V3. |
| 15 | Contract inherits `PrivacyGuarantees` (ethics mixin) and `ReentrancyGuard` (HIGH-001 fix). |
| 17–18 | `immutable` references to V1 identity registry and ZK verifier. |
| 20–26 | `ValidationType` — five values, identical to Contract 3. |
| 28–33 | `ValidationStatus` — **four values only**: PENDING, APPROVED, REJECTED, DISPUTED. **No CANCELLED**. This means no `cancelRequest` function exists; expired or dead requests cannot be cancelled and escrow cannot be returned to requesters after a deadline. |
| 36–49 | `ValidationRequest` struct — 12 fields, identical layout to Contract 3. No `deadline` field (V3 adds that). |
| 51–60 | `ValidationResponse` struct — 8 fields, identical to Contract 3. |
| 62–68 | Storage mappings — identical to Contract 3. |
| 70–71 | Constants: `MIN_VALIDATOR_STAKE = 1 ether`, `VALIDATION_REWARD = 0.1 ether`. Same values as Contract 3. |
| 73–78 | Six events — same signatures as Contract 3. |
| 80–85 | Constructor — same as Contract 3: two zero-checks, two immutable assignments. |
| 87–96 | `requestValidation` — `payable`, same logic as Contract 3. Requester must send `>= MIN_VALIDATOR_STAKE` (1 ether) as escrow. |
| 98–111 | `submitValidation` — **`payable`** (theloopbreaker variant allows inline stake top-up). This differs from all V3 variants where `submitValidation` is not payable. The validator's inline `msg.value` is stored in `response.validatorStake` but NOT added to `validatorStakes[validator]` — it is effectively lost (another bug in this variant). |
| 113–128 | `submitValidationZK` — calls `zkVerifier.verifyProof(proofBytes, publicInputs)` at line 125. **Critically does NOT check** `publicInputs[0] == uint256(request.claimHash)`. A validator can submit a valid ZK proof generated for a different claim and it will pass. This is the ZK proof binding omission. |
| 130–133 | `_checkValidationComplete` — majority-vote logic. No DISPUTED branch with active-count decrement (that fix belongs to V3.3). |
| 136–155 | `_distributeRewards` — **the underflow bug is at line 150**: `validatorStakes[validator] -= VALIDATION_REWARD`. The canonical V1 (Contract 3) uses `validatorStakes[validator] -= min(validatorStakes[validator], VALIDATION_REWARD)` to prevent underflow. This version omits the `min()` guard. Under Solidity 0.8 checked arithmetic, if `validatorStakes[validator] < VALIDATION_REWARD` (1e17 wei = 0.1 ether), the subtraction reverts, causing the entire `_distributeRewards` call to revert, blocking all reward distribution for all validators in that request. |
| 157–168 | Remaining functions — identical to Contract 3 (staking, withdrawal, view functions, paginated queries). |

---

### 4. State Variables

Identical to Contract 3 (see Contract 3 documentation). Key omissions vs V3:

- No `requestDeadlines` mapping (no per-request deadline enforcement)
- No `hasValidated` mapping (no duplicate response prevention — HIGH-VR-01 not fixed)
- No CANCELLED status in enum

---

### 5. Functions

All functions are structurally identical to Contract 3 except:

#### `submitValidation` — now `payable`

| Difference | Contract 3 (canonical V1) | Contract 19 (theloopbreaker) |
|-----------|--------------------------|------------------------------|
| `payable`? | No | Yes |
| Inline stake addition | N/A | `msg.value` stored in response but not added to `validatorStakes` |

#### `submitValidationZK` — missing claimHash binding

| Difference | Contract 3 | Contract 19 |
|-----------|-----------|-------------|
| ZK proof anchored to claim? | `require(publicInputs[0] == uint256(request.claimHash))` | No such check — proof valid for any claim |

#### `_distributeRewards` — underflow bug

| Difference | Contract 3 | Contract 19 |
|-----------|-----------|-------------|
| Slash arithmetic | `validatorStakes[validator] -= min(validatorStakes[validator], VALIDATION_REWARD)` | `validatorStakes[validator] -= VALIDATION_REWARD` (reverts if stake < 0.1 ether) |

---

### 6. Math

#### Underflow Condition (Bug)

```
VALIDATION_REWARD = 0.1 ether = 1e17 wei
MIN_VALIDATOR_STAKE = 1 ether = 1e18 wei

Scenario that triggers the bug:
1. Validator stakes 1 ether → validatorStakes[V] = 1e18
2. Validator is slashed once in request A → validatorStakes[V] = 1e18 - 1e17 = 9e17
3. Validator is slashed again in request B → validatorStakes[V] = 9e17 - 1e17 = 8e17
4. ...after 10 slashes: validatorStakes[V] = 0
5. Validator is slashed in request K while validatorStakes[V] = 0:
   → 0 - 1e17 → Solidity 0.8 checked arithmetic: REVERT
   → _distributeRewards reverts entirely
   → No validator in request K receives a reward or slash event
   → request K is permanently stuck in APPROVED/REJECTED with unreachable escrow

Fix (canonical V1): validatorStakes[V] -= min(validatorStakes[V], VALIDATION_REWARD)
→ min(0, 1e17) = 0 → subtraction is 0 -= 0 → no revert → graceful zero-slash
```

#### `submitValidation payable` — inline value fate

```
msg.value from submitValidation (theloopbreaker variant) → stored in response.validatorStake
validatorStakes[msg.sender] is NOT incremented
→ the ETH is locked in the contract forever (no path to return it to the validator)
→ unless rescueETH is present (it is not in V1 contracts)
```

---

### 7. Events

Same six events as Contract 3. No new events. No `ValidationCancelled` event (since CANCELLED status does not exist).

---

### 8. Security Invariants

**Violated Invariants (vs canonical V1 and V3):**

- **Underflow bug**: `_distributeRewards` can revert if any losing validator's stake is below `VALIDATION_REWARD`. When this happens, the entire reward distribution for that request is blocked — including ETH intended for honest majority validators. This is a denial-of-service on the distribution mechanism.
- **ZK proof not anchored**: A validator can reuse a valid ZK proof originally generated for claim A when submitting a response for claim B, as long as the proof is structurally valid per the verifier circuit. The ERC-8004 security model assumes ZK proofs are claim-specific.
- **No duplicate response prevention**: Unlike V3 (which adds `hasValidated`), this contract allows a single validator to submit multiple responses for the same request, manipulating the vote tally.
- **No request expiry**: Without CANCELLED status or `cancelRequest`, a PENDING request with insufficient validator participation will remain PENDING forever, locking the requester's escrow in the contract permanently.
- **Inline ETH lockup**: ETH sent via `payable submitValidation` is stored in the contract with no retrieval path.

---

### 9. ERC-8004 Standard Compliance

This contract implements the same ERC-8004 sections as Contract 3 (V1) but with the ZK proof binding omission reducing its §3.2 (ZK Proof Validation) compliance. The underflow bug and missing CANCELLED status are implementation defects rather than standard non-compliance per se, but they violate the operational invariants ERC-8004 assumes for a functioning validator economy.

---

### 10. How the Registries Fit Together

Contract 19 is a standalone deployment on the theloopbreaker site. It references the **V1** `ERC8004IdentityRegistry` (not V3). It is not part of the canonical VaultFire V3.3 deployment stack. The theloopbreaker deployment thus has a different trust chain: its identity registry does not include the V3 audit fixes (deactivation logic, agent capability hash, HIGH-IR-* fixes).

```
theloopbreaker site deployment:
  ERC8004IdentityRegistry (V1)     ← no HIGH-IR-* fixes
  ERC8004ValidationRegistry (C19)  ← this contract; has underflow bug
  [No ReputationRegistry referenced]
  [No Adapter referenced]
```

---

### 11. Dependencies and Rebuild Recipe

| Attribute | Value |
|-----------|-------|
| Compiler | `solc ^0.8.25` |
| Optimizer | 200 runs |
| External deps | `@openzeppelin/contracts/utils/ReentrancyGuard.sol` |
| Internal deps | `PrivacyGuarantees.sol`, `ERC8004IdentityRegistry.sol` (V1), `BeliefAttestationVerifier.sol` |
| Constructor args | `(address _identityRegistry, address _zkVerifier)` |
| Deploy after | `ERC8004IdentityRegistry` (V1), `BeliefAttestationVerifier` |
| **DO NOT deploy in production** | The underflow bug, missing ZK claim binding, and absent CANCELLED status make this contract unsuitable for production use. Use `contracts/ERC8004ValidationRegistry.sol` (Contract 3) or the V3.3 variant (Contract 16) instead. |


---

## Contract 20 — `VaultfireERC8004AdapterV3_decoded.sol`

### 1. Header

| Field | Value |
|-------|-------|
| **Filename** | `VaultfireERC8004AdapterV3_decoded.sol` |
| **Path** | `VaultfireERC8004AdapterV3_decoded.sol` (workspace root) |
| **Role** | Adapter — V3 intermediate ("decoded") version; has immediate `resetBondReputationSync` (HIGH-14 vulnerability) |
| **Solidity** | `^0.8.25` |
| **License** | MIT |

This contract is the **intermediate V3 adapter** that existed between the V3-PR-ready audit (Contract 11) and the V3.3 round-3 audit (Contract 17). It applies all the V3 base fixes (CRITICAL-AD-01 through MEDIUM-AD-02, CEI fix, `ValidationRequestCreated` event) and integrates the `AIPartnershipBondsV3` 10-field bond struct. However, it **has not yet received the HIGH-14 timelock fix**. Instead of the two-step `initiateBondSyncReset` / `finalizeBondSyncReset` mechanism with its 48-hour delay and `MAX_BOND_SYNCS` cap, this version exposes a single immediate function:

```solidity
function resetBondReputationSync(uint256 bondId) external onlyOwner {
    bondReputationSynced[bondId] = false;
}
```

This is the vulnerability that HIGH-14 addresses: a compromised or malicious owner key can call `resetBondReputationSync` at any time, instantly re-enabling `syncPartnershipReputation` for an already-synced bond. Because `syncPartnershipReputation` calls `reputationRegistry.submitVerifiedFeedback(...)`, this allows unlimited reputation inflation with zero governance delay. The "decoded" label in the filename indicates this was a human-readable artifact produced during the audit process (possibly decompiled or reconstructed from bytecode), explaining minor comment differences from the PR-ready version.

---

### 2. Full Source Code

```solidity
  1: // SPDX-License-Identifier: MIT
  2: pragma solidity ^0.8.25;
  3: 
  4: import "./ERC8004IdentityRegistryV3.sol";
  5: import "./ERC8004ReputationRegistryV3.sol";
  6: import "./ERC8004ValidationRegistryV3.sol";
  7: import "./AIPartnershipBondsV3.sol";
  8: 
  9: /**
 10:  * @title VaultFire ERC-8004 Adapter V3
 11:  * @notice Bridges VaultFire's AI Partnership Bonds with ERC-8004 standard
 12:  * @dev V3 — Applies audit fixes [same list as Contract 17, without V3.3 changes]
 13:  */
 14: contract VaultfireERC8004AdapterV3 {
 15: 
 16:     AIPartnershipBondsV3 public immutable partnershipBonds;
 17:     ERC8004IdentityRegistryV3 public immutable identityRegistry;
 18:     ERC8004ReputationRegistryV3 public immutable reputationRegistry;
 19:     ERC8004ValidationRegistryV3 public immutable validationRegistry;
 20: 
 21:     address public owner;
 22:     event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
 23:     modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; }
 24: 
 25:     mapping(address => bool) public autoRegisteredAgents;
 26:     mapping(uint256 => bool) public bondReputationSynced;
 27:     mapping(uint256 => bool) public bondValidationRequested;  // HIGH-AD-02
 28: 
 29:     // ⚠ NO bondSyncCount, NO MAX_BOND_SYNCS, NO bondSyncResetRequestedAt,
 30:     //   NO SYNC_RESET_TIMELOCK — HIGH-14 not yet applied
 31: 
 32:     event AgentAutoRegistered(address indexed agentAddress, string agentType, uint256 timestamp);
 33:     event PartnershipReputationSynced(uint256 indexed bondId, address indexed agentAddress, uint256 rating, uint256 timestamp);
 34:     event ValidationRequestCreated(uint256 indexed requestId, uint256 indexed bondId, address indexed agentAddress, uint256 timestamp);
 35:     // ⚠ NO BondSyncResetInitiated, NO BondSyncResetFinalized, NO PartnershipReputationRevoked
 36: 
 37:     constructor(
 38:         address _partnershipBonds, address _identityRegistry,
 39:         address _reputationRegistry, address _validationRegistry
 40:     ) {
 41:         require(_partnershipBonds != address(0), "Invalid partnership bonds");
 42:         require(_identityRegistry != address(0), "Invalid identity registry");
 43:         require(_reputationRegistry != address(0), "Invalid reputation registry");
 44:         require(_validationRegistry != address(0), "Invalid validation registry");
 45:         partnershipBonds = AIPartnershipBondsV3(_partnershipBonds);
 46:         identityRegistry = ERC8004IdentityRegistryV3(_identityRegistry);
 47:         reputationRegistry = ERC8004ReputationRegistryV3(_reputationRegistry);
 48:         validationRegistry = ERC8004ValidationRegistryV3(_validationRegistry);
 49:         owner = msg.sender;
 50:     }
 51: 
 52:     function transferOwnership(address newOwner) external onlyOwner {
 53:         require(newOwner != address(0), "Invalid new owner");
 54:         emit OwnershipTransferred(owner, newOwner);
 55:         owner = newOwner;
 56:     }
 57: 
 58:     receive() external payable {}
 59: 
 60:     function rescueETH() external onlyOwner {
 61:         uint256 balance = address(this).balance;
 62:         require(balance > 0, "No ETH to rescue");
 63:         (bool success, ) = payable(owner).call{value: balance}("");
 64:         require(success, "ETH rescue failed");
 65:     }
 66: 
 67:     function registerAgentForPartnership(
 68:         string calldata agentURI, string calldata agentType
 69:     ) external {
 70:         require(!autoRegisteredAgents[msg.sender], "Already registered for VaultFire");
 71:         require(bytes(agentURI).length > 0, "Agent URI required");
 72:         require(bytes(agentType).length > 0, "Agent type required");
 73:         if (!identityRegistry.isAgentActive(msg.sender)) {
 74:             revert("Agent must call identityRegistry.registerAgent() first");
 75:         }
 76:         autoRegisteredAgents[msg.sender] = true;
 77:         emit AgentAutoRegistered(msg.sender, agentType, block.timestamp);
 78:     }
 79: 
 80:     function syncPartnershipReputation(uint256 bondId) external {
 81:         require(!bondReputationSynced[bondId], "Already synced");
 82:         // ⚠ NO bondSyncCount check — no cap on sync count
 83:         (
 84:             ,
 85:             address human, address aiAgent, string memory partnershipType,
 86:             ,,,, bool active,
 87:         ) = partnershipBonds.bonds(bondId);
 88:         require(active, "Bond not active");
 89:         require(msg.sender == human || msg.sender == aiAgent, "Caller is not a bond participant");
 90:         require(aiAgent != address(0), "Invalid agent address");
 91:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered in ERC-8004");
 92:         uint256 rating = _calculatePartnershipRating(bondId);
 93:         bondReputationSynced[bondId] = true;   // CEI — before external call
 94:         // ⚠ NO bondSyncCount increment
 95:         reputationRegistry.submitVerifiedFeedback(aiAgent, rating, partnershipType, "", bondId);
 96:         emit PartnershipReputationSynced(bondId, aiAgent, rating, block.timestamp);
 97:     }
 98: 
 99:     // =========================================================================
100:     //  THE HIGH-14 VULNERABILITY: Immediate sync reset with no timelock or cap
101:     // =========================================================================
102: 
103:     /// @dev [audit-v3] HIGH-AD-04: Allow owner to reset sync for emergency corrections
104:     function resetBondReputationSync(uint256 bondId) external onlyOwner {
105:         bondReputationSynced[bondId] = false;
106:     }
107: 
108:     // ⚠ NO revokePartnershipReputation (HIGH-17 not yet applied)
109: 
110:     function _calculatePartnershipRating(uint256 bondId) internal view returns (uint256 rating) {
111:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
112:         require(qualityScore <= 10000, "Quality score out of range");
113:         return qualityScore;
114:     }
115: 
116:     function requestPartnershipValidation(
117:         uint256 bondId, string calldata claimURI,
118:         ERC8004ValidationRegistryV3.ValidationType validationType
119:     ) external payable {
120:         require(bytes(claimURI).length > 0, "Claim URI required");
121:         require(!bondValidationRequested[bondId], "Validation already requested for this bond");
122:         (,, address aiAgent,,,,, bool active,) = partnershipBonds.bonds(bondId);
123:         require(active, "Bond not active");
124:         require(aiAgent != address(0), "Invalid agent address");
125:         require(identityRegistry.isAgentActive(aiAgent), "Agent not registered");
126:         uint256 qualityScore = partnershipBonds.partnershipQualityScore(bondId);
127:         bytes32 claimHash = keccak256(abi.encodePacked(bondId, qualityScore));
128:         bondValidationRequested[bondId] = true;    // CEI
129:         uint256 expectedRequestId = validationRegistry.nextRequestId();
130:         validationRegistry.requestValidation{value: msg.value}(
131:             aiAgent, claimURI, claimHash, validationType,
132:             validationType == ERC8004ValidationRegistryV3.ValidationType.MULTI_VALIDATOR ? 3 : 1
133:         );
134:         emit ValidationRequestCreated(expectedRequestId, bondId, aiAgent, block.timestamp);
135:     }
136: 
137:     function getAgentCrossPlatformReputation(address agentAddress)
138:         external view returns (
139:             uint256 vaultfireRating, uint256 erc8004Rating,
140:             uint256 totalFeedbacks, uint256 verifiedPercentage) {
141:         require(agentAddress != address(0), "Invalid agent address");
142:         vaultfireRating = 0;
143:         (erc8004Rating, totalFeedbacks, , ) = reputationRegistry.getReputation(agentAddress);
144:         verifiedPercentage = reputationRegistry.getVerifiedFeedbackPercentage(agentAddress);
145:     }
146: 
147:     function isAgentFullyRegistered(address agentAddress)
148:         external view returns (bool registeredERC8004, bool registeredVaultFire) {
149:         require(agentAddress != address(0), "Invalid agent address");
150:         registeredERC8004 = identityRegistry.isAgentActive(agentAddress);
151:         registeredVaultFire = autoRegisteredAgents[agentAddress];
152:     }
153: 
154:     function discoverVaultfireAgents(string calldata agentType)
155:         external pure returns (address[] memory agents) {
156:         agentType;
157:         return new address[](0);
158:     }
159: }
```

---

### 3. Line-by-Line Walkthrough

| Lines | Explanation |
|-------|-------------|
| 1–7 | SPDX + pragma + imports. Same four imports as Contract 17: V3 registries plus `AIPartnershipBondsV3`. |
| 14 | Same contract name: `VaultfireERC8004AdapterV3`. This is a naming collision with Contract 17 — they cannot be deployed from the same compilation unit. |
| 16–19 | Four `immutable` registry references — identical to Contract 17. |
| 21–23 | `owner`, `OwnershipTransferred`, `onlyOwner` — identical to Contract 17. |
| 25–27 | Three mappings: `autoRegisteredAgents`, `bondReputationSynced`, `bondValidationRequested`. **Absent**: `bondSyncCount`, `bondSyncResetRequestedAt`. |
| 29–30 | Comment callout: HIGH-14 constants and mappings are intentionally absent in this intermediate version. |
| 32–34 | Three events: `AgentAutoRegistered`, `PartnershipReputationSynced`, `ValidationRequestCreated`. **Absent**: `BondSyncResetInitiated`, `BondSyncResetFinalized`, `PartnershipReputationRevoked`. |
| 37–50 | Constructor — identical to Contract 17. |
| 52–56 | `transferOwnership` — identical to Contract 17. |
| 58 | `receive()` — HIGH-AD-03 fix present. |
| 60–65 | `rescueETH()` — HIGH-AD-03 fix present. |
| 67–78 | `registerAgentForPartnership` — identical to Contract 17. |
| 80–97 | `syncPartnershipReputation` — the **key difference** vs Contract 17: line 82 has no `bondSyncCount` check; line 94 has no `bondSyncCount += 1`. The sync can proceed an unlimited number of times if `bondReputationSynced` is reset by the owner. |
| 99–106 | **THE HIGH-14 VULNERABILITY.** `resetBondReputationSync(uint256 bondId)` — single line `bondReputationSynced[bondId] = false`. No timelock, no count check, no event with future availability time. An owner (or compromised key) can call this at will, enabling immediate re-sync. |
| 108 | `revokePartnershipReputation` is absent (HIGH-17 not applied). There is no way to revoke a synced reputation in this version. |
| 110–114 | `_calculatePartnershipRating` — identical to Contract 17. |
| 116–135 | `requestPartnershipValidation` — identical to Contract 17. |
| 137–145 | `getAgentCrossPlatformReputation` — identical to Contract 17. |
| 147–152 | `isAgentFullyRegistered` — identical to Contract 17. |
| 154–158 | `discoverVaultfireAgents` — MEDIUM-AD-01 shim, identical to Contract 17. |

---

### 4. State Variables

| Name | Type | Vs. Contract 17 |
|------|------|----------------|
| `partnershipBonds` | `AIPartnershipBondsV3 immutable` | Same |
| `identityRegistry` | `ERC8004IdentityRegistryV3 immutable` | Same |
| `reputationRegistry` | `ERC8004ReputationRegistryV3 immutable` | Same |
| `validationRegistry` | `ERC8004ValidationRegistryV3 immutable` | Same |
| `owner` | `address` | Same |
| `autoRegisteredAgents` | `mapping(address => bool)` | Same |
| `bondReputationSynced` | `mapping(uint256 => bool)` | Same — but reset path differs (immediate vs timelocked) |
| `bondValidationRequested` | `mapping(uint256 => bool)` | Same |
| `bondSyncCount` | **ABSENT** | Added in Contract 17 |
| `MAX_BOND_SYNCS` | **ABSENT** | Added in Contract 17 |
| `bondSyncResetRequestedAt` | **ABSENT** | Added in Contract 17 |
| `SYNC_RESET_TIMELOCK` | **ABSENT** | Added in Contract 17 |

---

### 5. Functions

The complete divergence vs. Contract 17 is the pair of functions that manage the sync reset:

#### `resetBondReputationSync(uint256 bondId)` — THE HIGH-14 VULNERABILITY

| Attribute | Value |
|-----------|-------|
| Signature | `resetBondReputationSync(uint256)` |
| Visibility | `external` |
| Modifiers | `onlyOwner` |
| State changes | `bondReputationSynced[bondId] = false` |
| Events | None |
| Reverts | Not owner |
| Security risk | Owner can call this at any time, any number of times, for any bond, with zero delay. Each call allows another `syncPartnershipReputation` call, which writes another verified feedback entry to the reputation registry. No cap exists. An attacker controlling the owner key can inflate any agent's reputation score indefinitely by cycling: `resetBondReputationSync` → `syncPartnershipReputation` → `resetBondReputationSync` → ... |

**Absent functions (vs. Contract 17):**
- `initiateBondSyncReset` — does not exist
- `finalizeBondSyncReset` — does not exist
- `revokePartnershipReputation` — does not exist

All other functions are identical to Contract 17.

---

### 6. Math

No new math vs. Contract 17. The HIGH-14 vulnerability is behavioral, not arithmetic:

```
Reputation inflation attack (no limit without bondSyncCount cap):

Iteration 1: syncPartnershipReputation(bondId=5)
  → bondReputationSynced[5] = true
  → reputationRegistry: agent.totalFeedbacks += 1, rating updated

Owner calls: resetBondReputationSync(5)
  → bondReputationSynced[5] = false (no delay, no events, no count check)

Iteration 2: syncPartnershipReputation(bondId=5)
  → bondReputationSynced[5] = true again
  → reputationRegistry: agent.totalFeedbacks += 1 AGAIN, rating updated AGAIN
  → agent now has 2 verified feedbacks from 1 bond

...repeat indefinitely...

After N cycles: agent has N verified feedback records from a single bond.
Because totalFeedbacks grows, the EMA formula weights new (manipulated) ratings more heavily,
and verifiedFeedbackPercentage is inflated.

Contract 17 fixes this: MAX_BOND_SYNCS = 3 prevents cycle count > 3;
SYNC_RESET_TIMELOCK = 48h requires governance-level delay.
```

---

### 7. Events

Same three events as the V3-PR-ready adapter (Contract 11). Missing the four V3.3 events.

| Present | Absent |
|---------|--------|
| `AgentAutoRegistered` | `BondSyncResetInitiated` |
| `PartnershipReputationSynced` | `BondSyncResetFinalized` |
| `ValidationRequestCreated` | `PartnershipReputationRevoked` |
| `OwnershipTransferred` | — |

The absence of `BondSyncResetInitiated` / `BondSyncResetFinalized` means resets are not observable on-chain via events — there is no audit trail for when or how many times an owner has reset a bond's sync flag.

---

### 8. Security Invariants

**Violated by this contract vs. Contract 17:**

- **Sync count is not bounded.** A bond can be synced an unlimited number of times with no on-chain prevention. Only the `onlyOwner` check on `resetBondReputationSync` limits this, which provides no protection against a compromised or malicious key.
- **Reset is instantaneous and silent.** No timelock, no event with a "available at" timestamp. Off-chain monitoring systems have no opportunity to observe and intervene before the reset takes effect.
- **No reputation revocation path.** Incorrectly synced bond reputations (from early-exit bonds or domination-penalty bonds) cannot be revoked. The only recourse is a direct call to `reputationRegistry.revokeVerifiedFeedback` by the reputation registry owner — a separate key from the adapter owner.

**Invariants that ARE upheld (same as Contract 17):**
- CRITICAL-AD-01: only bond participants can initiate sync
- CEI pattern on `bondReputationSynced` and `bondValidationRequested`
- HIGH-AD-01: deterministic `claimHash` (no timestamp)
- HIGH-AD-02: one validation request per bond
- HIGH-AD-03: ETH rescue path

---

### 9. ERC-8004 Standard Compliance

Same as Contract 17 except HIGH-14 and HIGH-17 are not addressed. The reputation portability guarantee of ERC-8004 §4 is weakened by the unlimited re-sync capability.

---

### 10. How the Registries Fit Together

This contract occupies the same position as Contract 17 in the architecture diagram but with the vulnerability path active:

```
[Owner or attacker with owner key]
           │
           ▼
VaultfireERC8004AdapterV3 (decoded — this contract)
    resetBondReputationSync(bondId) ← no delay, no limit
           │ bondReputationSynced[bondId] = false
           ▼
    syncPartnershipReputation(bondId) [called by bond participant]
           │ bondReputationSynced[bondId] = true
           │ (no bondSyncCount increment)
           ▼
ERC8004ReputationRegistryV3.submitVerifiedFeedback(...)
    → agent.totalFeedbacks += 1 [again, no cap]
    → rating re-calculated [can be repeatedly biased]
```

---

### 11. Dependencies and Rebuild Recipe

Same as Contract 17:

| Attribute | Value |
|-----------|-------|
| Compiler | `solc ^0.8.25` |
| Optimizer | 200 runs |
| External deps | None |
| Internal deps | `ERC8004IdentityRegistryV3`, `ERC8004ReputationRegistryV3`, `ERC8004ValidationRegistryV3`, `AIPartnershipBondsV3` |
| Constructor args | `(address _partnershipBonds, address _identityRegistry, address _reputationRegistry, address _validationRegistry)` |
| **STATUS** | Superseded by Contract 17 (`vf-audit-contracts/VaultfireERC8004AdapterV3.sol`). Do not deploy in production; HIGH-14 vulnerability is present. |

### Version Comparison Summary: Decoded (C20) vs V3.3 (C17)

| Feature | Contract 20 (decoded) | Contract 17 (V3.3) |
|---------|----------------------|-------------------|
| Sync reset mechanism | `resetBondReputationSync` — immediate | `initiateBondSyncReset` + `finalizeBondSyncReset` — 48h timelock |
| Sync count tracking | None | `bondSyncCount` per bond |
| Max syncs per bond | None (unlimited) | 3 (`MAX_BOND_SYNCS`) |
| Timelock constant | None | `SYNC_RESET_TIMELOCK = 48 hours` |
| Reset events | None | `BondSyncResetInitiated`, `BondSyncResetFinalized` |
| Reputation revocation | None | `revokePartnershipReputation` (HIGH-17) |
| `PartnershipReputationRevoked` event | Absent | Present |
| HIGH-14 status | Vulnerable | Fixed |
| HIGH-17 status | Not addressed | Fixed |


---

# Global Math Reference — ERC-8004 Numeric Computations

This section consolidates every numeric computation across all 20 contracts into a single reference. All values are in Solidity `uint256` unless noted.

---

## 1. Rating Scale

```
Range: 0 to 10000 (inclusive)
Units: basis points (1 basis point = 0.01%)
0     = lowest possible rating (completely untrustworthy)
5000  = neutral / unknown (Bayesian prior center)
10000 = highest possible rating (perfect)

Examples:
  7500  = 75.00% quality score
  9000  = 90.00% quality score
  10000 = 100.00% quality score (maximum)
```

The 10000 scale is used by both the ERC-8004 reputation registry and VaultFire's `AIPartnershipBonds.partnershipQualityScore`. The adapter's `_calculatePartnershipRating` passes the quality score through unchanged — the two scales are identical.

---

## 2. Reputation Algorithm — Three-Phase Logic

The `ERC8004ReputationRegistryV3._updateReputation(agentAddress, newRating)` function (and all V3 variants) implements a three-phase algorithm based on `totalFeedbacks`:

```
Let N = agent.totalFeedbacks (BEFORE the new feedback is added)
Let R_old = agent.averageRating (current value)
Let R_new = newRating (incoming, 0–10000)
Let MAX_TRUE_AVG = MAX_FEEDBACKS_FOR_TRUE_AVG = 1000  (private constant)
```

### Phase 1 — First Feedback (N == 0)

```
R_result = (5000 + R_new) / 2

Purpose: Bayesian prior — anchors a new agent's reputation midway between
the neutral prior (5000) and the single data point, preventing extreme
first-impression effects.

Example: first feedback = 8000
R_result = (5000 + 8000) / 2 = 13000 / 2 = 6500

Example: first feedback = 2000
R_result = (5000 + 2000) / 2 = 7000 / 2 = 3500
```

### Phase 2 — True Average (1 <= N < 1000)

```
R_result = (R_old * N + R_new) / (N + 1)

This is a standard cumulative moving average. Integer division truncates.

Example: agent has N=3 feedbacks, R_old=7000, R_new=9000:
R_result = (7000 * 3 + 9000) / (3 + 1)
         = (21000 + 9000) / 4
         = 30000 / 4
         = 7500

Example: agent has N=999 feedbacks, R_old=6800, R_new=4000:
R_result = (6800 * 999 + 4000) / 1000
         = (6793200 + 4000) / 1000
         = 6797200 / 1000
         = 6797 (integer division — truncates)
```

### Phase 3 — Exponential Moving Average (N >= 1000)

```
R_result = (R_old * 9 + R_new) / 10

The EMA weights recent feedback at 10% and existing average at 90%.
This prevents late feedback from dramatically changing a well-established
agent's reputation. Equivalent to EMA with α = 0.1 (smooth factor).

Example: R_old=7200, R_new=3000 (hostile feedback):
R_result = (7200 * 9 + 3000) / 10
         = (64800 + 3000) / 10
         = 67800 / 10
         = 6780

Example: R_old=7200, R_new=9000 (excellent feedback):
R_result = (7200 * 9 + 9000) / 10
         = (64800 + 9000) / 10
         = 73800 / 10
         = 7380

Convergence: an agent at R_old=7200 receiving constant R_new=9000
converges toward 9000 asymptotically:
  After 1: 7380
  After 2: 7542
  After 3: 7688
  After N: 9000 - (9000 - 7200) * (0.9)^N
```

---

## 3. Verified Feedback Percentage

Computed by `getVerifiedFeedbackPercentage(address agentAddress)`:

```
verifiedPercentage = (verifiedFeedbacks * 100) / totalFeedbacks

Units: percentage (0–100, integer, truncated)

Example: 45 verified out of 60 total:
  = (45 * 100) / 60 = 4500 / 60 = 75

Example: 0 verified out of 10 total:
  = (0 * 100) / 10 = 0

Edge case: totalFeedbacks == 0
  → function returns 0 (explicit guard against division by zero)
```

---

## 4. Validator Staking Constants

```
MIN_VALIDATOR_STAKE = 1 ether = 1_000_000_000_000_000_000 wei = 1e18 wei

VALIDATION_REWARD   = 0.1 ether = 100_000_000_000_000_000 wei = 1e17 wei

Ratio: VALIDATION_REWARD / MIN_VALIDATOR_STAKE = 0.1 (10%)

Maximum slashes before stake depleted (assuming no withdrawals or top-ups):
  floor(MIN_VALIDATOR_STAKE / VALIDATION_REWARD) = floor(1e18 / 1e17) = 10

i.e., a validator staking exactly the minimum can be slashed at most 10 times
before their stake reaches zero.
```

---

## 5. Slash Arithmetic

### Canonical V1 and V3 (safe — with min() guard)

```solidity
validatorStakes[validator] -= Math.min(validatorStakes[validator], VALIDATION_REWARD);
```

```
Let S = validatorStakes[validator] before slash
Let R = VALIDATION_REWARD = 1e17

Case S >= R:  subtracted = R;  S_after = S - R  (normal slash)
Case S < R:   subtracted = S;  S_after = S - S = 0  (graceful floor at 0)
Case S == 0:  subtracted = 0;  S_after = 0  (no-op — idempotent)

Example (normal): S = 8e17, R = 1e17 → S_after = 7e17
Example (near-zero): S = 5e16, R = 1e17 → subtracted = 5e16 → S_after = 0
```

### theloopbreaker V1 (Contract 19 — UNSAFE — missing min() guard)

```solidity
validatorStakes[validator] -= VALIDATION_REWARD;
```

```
Case S >= R:  normal — fine
Case S < R:   Solidity 0.8 checked arithmetic → REVERT
              → _distributeRewards call reverts entirely
              → all rewards for the request are blocked (DoS)
              → requester's escrow remains stuck in the contract
```

---

## 6. Reward Distribution (V1 and V3)

```
Each majority validator receives VALIDATION_REWARD = 0.1 ether from the contract's ETH balance.
The contract accumulates ETH via requestValidation (stakeAmount = msg.value >= 1 ether).

One full request cycle:
  In:  requester deposits 1 ether (MIN_VALIDATOR_STAKE) as escrow
  Out (APPROVED, 3-validator): 3 majority validators × 0.1 ether = 0.3 ether paid out
       1 minority validator:   slashed 0.1 ether (stake reduced, no ETH paid)
  Net: 1.0 - 0.3 = 0.7 ether remaining in contract for future rewards
       (plus the 0.1 ether slash credit if we treat the stake as in-contract)

Note: The escrow system (CRITICAL-VR-01 fix in V3.2) changes this:
  Each request now uses only its own stakeAmount for rewards, preventing cross-request
  depletion. But the stakeAmount is still msg.value from requestValidation, and
  rewards (VALIDATION_REWARD = 0.1 ether) are fixed, not per-request proportional.
  If many validators participate and the stakeAmount is small, the contract may run out
  of ETH for rewards — this is a known limitation noted in V3.3 comments.
```

---

## 7. Feedback Cooldown

```
FEEDBACK_COOLDOWN = 1 hours = 3600 seconds

Enforced in: ERC8004ReputationRegistryV3.submitFeedback (non-authorized)

condition: block.timestamp >= lastFeedbackTime[from][agentAddress] + 3600

Example: feedback at T=1000000
  next allowed: T >= 1003600
  too early attempt at T=1001000: 1001000 < 1003600 → REVERT
  valid attempt at T=1003600: 1003600 >= 1003600 → OK
```

---

## 8. Authorized Feedback Cooldown

```
MIN_AUTHORIZED_FEEDBACK_INTERVAL = 1 hours = 3600 seconds

Enforced in: ERC8004ReputationRegistryV3.submitVerifiedFeedback

condition: block.timestamp >= lastAuthorizedFeedbackTime[agentAddress] + 3600

Unlike FEEDBACK_COOLDOWN (which is per caller-agent pair), this is per agentAddress only.
Even with a new caller (different authorized contract), the same agent cannot receive two
verified feedbacks within 3600 seconds. This prevents rapid reputation inflation via
multiple authorized callers operating in the same block.
```

---

## 9. Request Deadline (V3)

```
REQUEST_DEADLINE = 7 days = 604800 seconds

Enforced in: ERC8004ValidationRegistryV3.cancelRequest

condition: block.timestamp > request.createdAt + 604800

If a validation request is created at T and receives insufficient validators,
the requester may call cancelRequest after T + 604800 to recover their escrow.

Status must be PENDING for cancellation to proceed.
```

---

## 10. Timelock and Sync Count (V3.3 Adapter)

```
MAX_BOND_SYNCS = 3
SYNC_RESET_TIMELOCK = 48 hours = 172800 seconds

Sync lifecycle for a bond:
  State:  bondSyncCount[bondId] = 0 (initial)
          bondReputationSynced[bondId] = false

  1st sync: bondSyncCount becomes 1, bondReputationSynced = true
  initiate reset: bondSyncResetRequestedAt = T₁
  finalize reset (after T₁+172800): bondReputationSynced = false, requestedAt = 0
  2nd sync: bondSyncCount becomes 2, bondReputationSynced = true
  initiate reset: bondSyncResetRequestedAt = T₂
  finalize reset (after T₂+172800): bondReputationSynced = false, requestedAt = 0
  3rd sync: bondSyncCount becomes 3, bondReputationSynced = true
  ← no further resets allowed: bondSyncCount (3) < MAX_BOND_SYNCS (3) is false
  ← no further syncs possible via this path

Total maximum verified feedbacks from one bond = 3
```

---

## 11. Overflow / Underflow Analysis

All contracts use `pragma solidity ^0.8.25`, which enables checked arithmetic by default. Overflows and underflows revert automatically unless wrapped in `unchecked {}` (not used in these contracts).

| Expression | Safe? | Reasoning |
|-----------|-------|-----------|
| `R_old * N` in Phase 2 | Yes | `R_old <= 10000`, `N <= 999`; max = 10000 * 999 = 9,990,000 — well within uint256 |
| `R_old * 9` in Phase 3 | Yes | `R_old <= 10000`; max = 90,000 |
| `verifiedFeedbacks * 100` | Yes | max = 2^256/100 feedbacks before overflow; practically impossible |
| `validatorStakes[v] -= VALIDATION_REWARD` (C19) | NO | Can underflow if stake < 0.1 ether — reverts in 0.8+ |
| `bondSyncCount[bondId] += 1` | Yes | Cap enforced at 3 before increment |
| `block.timestamp + SYNC_RESET_TIMELOCK` | Yes | `block.timestamp + 172800` overflows at year ~292 billion |
| `nextRequestId++` | Yes | Would need 2^256 requests to overflow |


---

# Security Invariants — Whole System

This section documents the security properties that must hold across all ERC-8004 contracts as a system, not just within individual contracts.

---

## 1. ETH Custody

**Identity Registry (all versions)**
- Holds no ETH. `registerAgent`, `deactivateAgent`, `updateAgentURI` are not payable. There is no ETH accumulation path.
- Invariant: `address(identityRegistry).balance == 0` at all times.

**Reputation Registry (all versions)**
- Holds no ETH. `submitFeedback`, `submitVerifiedFeedback`, `revokeVerifiedFeedback` are not payable.
- Invariant: `address(reputationRegistry).balance == 0` at all times.

**Validation Registry (V1 and V3)**
- Holds ETH intentionally: validator stakes (`stakeAsValidator`) and requester escrow (`requestValidation`).
- Invariant: `address(validationRegistry).balance >= sum(validatorStakes[all v]) + sum(active escrow[all requests])` — the registry must always have enough ETH to honor pending withdrawals and rewards.
- A live vulnerability in Contract 19 (theloopbreaker) can cause the distribution loop to revert, trapping ETH indefinitely.

**Adapter (V3 and V3.3)**
- Designed to hold no ETH intentionally. `receive()` is present only to accept ETH forwarded for validation escrow.
- `rescueETH()` provides an emergency drain path.
- Invariant: under normal operation, `address(adapter).balance == 0` after each `requestPartnershipValidation` call (ETH is forwarded to the validation registry in the same transaction).

---

## 2. Identity Invariants

- An agent address can be registered at most once per registry instance (`require(!agents[agentAddress].exists)`).
- Re-registration after deactivation is explicitly allowed in V1 (agents can rejoin after leaving). In V3, re-registration is blocked: `require(!agents[agentAddress].exists)` — the V3 identity registry stores deactivated agents and prevents re-registration using the same address, preventing Sybil recovery via deactivation cycling.
- `isAgentActive` returns `false` for unregistered addresses AND for deactivated agents. Both conditions are handled explicitly in V3.
- Only the agent themselves can call `registerAgent` (uses `msg.sender` as the agent address). No third party can register an agent on their behalf.
- Only the agent themselves can call `deactivateAgent` (uses `msg.sender`). No third party can deactivate an agent.
- `updateAgentURI` is agent-only. The registry owner cannot modify an agent's URI.

---

## 3. Reputation Invariants

- `averageRating` is always in [0, 10000] after any valid `_updateReputation` call, because:
  - Phase 1: `(5000 + R_new) / 2` with `R_new <= 10000` → max result = (5000+10000)/2 = 7500 ≤ 10000
  - Phase 2: weighted average of values in [0, 10000] stays in [0, 10000]
  - Phase 3: `(R_old * 9 + R_new) / 10` with both operands in [0, 10000] → max = (90000+10000)/10 = 10000
- `totalFeedbacks` never decreases except via `revokeVerifiedFeedback`. In V3.3, revocation is the only decrement path.
- `verifiedFeedbacks <= totalFeedbacks` at all times (verified feedbacks are a subset of total).
- `submitFeedback` is rate-limited per caller-agent pair (FEEDBACK_COOLDOWN = 1h). Prevents rapid-fire non-authorized reputation manipulation.
- `submitVerifiedFeedback` is rate-limited per agent (MIN_AUTHORIZED_FEEDBACK_INTERVAL = 1h). Prevents multi-source rapid inflation even with multiple authorized callers.
- `submitVerifiedFeedback` requires `msg.sender` to be in `authorizedCallers`. Only the reputation registry owner can add/remove authorized callers via `setAuthorizedCaller`.

---

## 4. Validation Invariants

**Stake integrity**
- A validator cannot `withdrawValidatorStake` if `validatorActiveValidations[validator] > 0`. Stakes are locked while validation is in progress.
- After any terminal state (APPROVED, REJECTED, DISPUTED in V3, CANCELLED in V3.3), `validatorActiveValidations` is decremented for all participants. This ensures the withdrawal path is eventually unblocked.
- V3.3 (Contract 16) fixes the case where DISPUTED or CANCELLED did not decrement active counts — this was a permanent stake lockout bug.

**Duplicate prevention (V3+)**
- `hasValidated[requestId][validator]` prevents a single validator from submitting multiple responses for the same request. Without this guard, a single validator could dominate the vote tally.

**Request lifecycle**
- PENDING → APPROVED, REJECTED, DISPUTED (all V1/V3 when total responses == validatorsRequired)
- PENDING → CANCELLED (V3 only, via `cancelRequest` after deadline)
- Terminal states are irreversible: no function transitions from APPROVED/REJECTED/DISPUTED/CANCELLED back to PENDING.

**Escrow isolation (V3.2+ fix — CRITICAL-VR-01)**
- Each validation request escrow is accounted separately. Rewards are paid from the specific request's stakeAmount, not from a global pool. Prevents one heavily-funded request from subsidizing another.

---

## 5. Adapter Invariants

**CRITICAL-AD-01 (V3+)**
- `syncPartnershipReputation` can only be called by the bond's `human` or `aiAgent`. An arbitrary EOA or contract cannot trigger reputation writes for a bond they are not party to.

**CEI pattern**
- In `syncPartnershipReputation`: `bondReputationSynced[bondId] = true` is written before `reputationRegistry.submitVerifiedFeedback(...)`. If the external call were to somehow re-enter `syncPartnershipReputation`, the `require(!bondReputationSynced[bondId])` guard would trigger.
- In `requestPartnershipValidation`: `bondValidationRequested[bondId] = true` is written before `validationRegistry.requestValidation(...)`.

**HIGH-14 (V3.3 only)**
- After `bondSyncCount[bondId] == 3`, no path can ever allow another sync for that bond (neither `syncPartnershipReputation` nor `initiateBondSyncReset` will succeed).
- Between `initiateBondSyncReset` and `finalizeBondSyncReset`, the bond remains marked as synced (`bondReputationSynced[bondId] == true`). The reset is pending but not applied.
- If a pending reset expires (no `finalizeBondSyncReset` called), the pending state persists indefinitely. A second `initiateBondSyncReset` call reverts (`require(bondSyncResetRequestedAt[bondId] == 0)`), preventing stacked resets.

**HIGH-17 (V3.3 only)**
- `revokePartnershipReputation` requires `bondReputationSynced[bondId] == true`. Cannot revoke a bond that was never synced, preventing spurious decrement of an agent's reputation.

---

## 6. Authorization Trust Chain

The ERC-8004 system enforces a layered trust model:

```
Registry Owner (multisig recommended)
    │
    ├── can call: identityRegistry.transferOwnership()
    ├── can call: reputationRegistry.setAuthorizedCaller(adapter, true/false)
    ├── can call: reputationRegistry.transferOwnership()
    └── can call: adapter.transferOwnership(), adapter.rescueETH(),
                  adapter.revokePartnershipReputation(),
                  adapter.initiateBondSyncReset(), adapter.finalizeBondSyncReset()

Authorized Callers (set by Reputation Registry Owner)
    └── VaultfireERC8004AdapterV3 (must be added post-deploy)
            └── can call: reputationRegistry.submitVerifiedFeedback()
                          reputationRegistry.revokeVerifiedFeedback()

Bond Participants (human or aiAgent in the bond)
    └── can call: adapter.syncPartnershipReputation(bondId)

Any Address
    └── can call: identityRegistry.registerAgent()
                  identityRegistry.deactivateAgent() [own agent only]
                  identityRegistry.updateAgentURI() [own agent only]
                  reputationRegistry.submitFeedback() [rate-limited]
                  validationRegistry.stakeAsValidator()
                  validationRegistry.withdrawValidatorStake()
                  validationRegistry.requestValidation() [payable]
                  validationRegistry.submitValidation()
                  validationRegistry.submitValidationZK()
```

No escalation path exists from lower tiers to higher tiers. An agent cannot grant themselves authorized-caller status.

---

## 7. Sybil Resistance

The ERC-8004 design includes multiple Sybil resistance mechanisms:

- **One address, one identity.** Re-registration is blocked in V3 (V1 allows re-registration but the previous rating history is reset — both start at 5000 Bayesian prior on first feedback).
- **Economic stake required for validation.** `MIN_VALIDATOR_STAKE = 1 ether` prevents free Sybil validators.
- **Slash-on-minority-vote.** Sybil validators colluding in a minority will lose 0.1 ether per participation.
- **Rate limiting on feedback.** A Sybil network of non-authorized accounts is rate-limited to one feedback per account per hour per target agent.
- **Authorized callers for verified feedback.** Only contracts added by the registry owner can submit high-weight verified feedback. A Sybil account cannot directly call `submitVerifiedFeedback`.
- **Bond participant check on sync.** To sync a bond's reputation, the caller must be a party to the bond itself — a Sybil cannot sync arbitrary bonds.

---

## 8. Admin Key Risk

All V3 and V3.3 contracts have an `owner` role. The primary residual risk is a compromised owner key. Mitigations:

- V3.3 adapter: `initiateBondSyncReset` + 48h timelock provides governance window for detecting compromise before a reset takes effect.
- `revokePartnershipReputation` is irreversible in the direction of correction — a compromised key can revoke legitimate reputation, but the reputation registry owner (a separate key) can be updated.
- V3 identity registry: `transferOwnership` to a multisig is recommended post-deploy.
- No owner privilege exists to modify individual agent registrations (no `forceDeactivate`, no `overwriteRating`). Agents retain sovereignty over their own identity data.

---

## 9. Re-entrancy Analysis

| Contract | ETH outflows | CEI applied? | Guard? |
|---------|-------------|-------------|--------|
| Identity Registry | None | N/A | N/A |
| Reputation Registry | None | N/A | N/A |
| Validation Registry V1/V3 | `withdrawValidatorStake`, `_distributeRewards` | Yes (stake decremented before `.call`) | `ReentrancyGuard` |
| Adapter V3/V3.3 | `rescueETH` | N/A (balance drained entirely) | `onlyOwner` |
| Adapter V3/V3.3 | forwarding in `requestPartnershipValidation` | CEI on `bondValidationRequested` | — |

No contract holds ETH-receiving storage that could be re-entered after a `.call` without a state guard already in place.


---

# Dependencies and Rebuild Recipe

This section provides the complete information needed to compile, test, and deploy the ERC-8004 system from source.

---

## 1. Compiler Requirements

```
Compiler:  solc (Solidity compiler)
Version:   ^0.8.25 (≥ 0.8.25, < 0.9.0)
Mode:      Checked arithmetic (default in 0.8+, no unchecked{} blocks in these contracts)
Optimizer: Enabled, 200 runs
Target:    EVM default (Paris or later; Shanghai for PUSH0)

Recommended exact version: solc 0.8.25
```

---

## 2. External Dependencies

| Package | Version constraint | Used by |
|---------|-------------------|---------|
| `@openzeppelin/contracts` | `^4.x` or `^5.x` | `ReentrancyGuard` imported in all Validation Registries |

Only `ReentrancyGuard.sol` from OpenZeppelin is imported. No other OZ contracts are used.

```
npm install @openzeppelin/contracts
# or
yarn add @openzeppelin/contracts
```

---

## 3. Internal Dependencies (VaultFire-specific)

These files are not part of the ERC-8004 standard but are required to compile VaultFire's deployment:

| File | Used by |
|------|---------|
| `PrivacyGuarantees.sol` | Identity Registry (all versions), Validation Registry (all versions) |
| `MissionEnforcement.sol` | Referenced in NatSpec/comments; may be imported in some variants |
| `BeliefAttestationVerifier.sol` | Validation Registry (all versions) — ZK proof verifier |
| `AIPartnershipBondsV2.sol` | Adapter V3-PR-ready (Contract 11) |
| `AIPartnershipBondsV3.sol` | Adapter V3.3 (Contract 17), Adapter V3-decoded (Contract 20) |
| `IERC8004IdentityRegistry.sol` | `AIPartnershipBondsV3` (HIGH-16 fix) |

---

## 4. Deployment Order

The contracts have strict dependency ordering. Deploy in this sequence:

```
Step 1:  BeliefAttestationVerifier
         Constructor: (implementation-specific)
         No ERC-8004 dependencies.

Step 2:  ERC8004IdentityRegistryV3  [or V1 for theloopbreaker]
         Constructor: ()  (no arguments)
         Depends on: PrivacyGuarantees (base contract, same compilation unit)

Step 3:  ERC8004ReputationRegistryV3  [or V1]
         Constructor: (address identityRegistry)
         Depends on: Step 2 address

Step 4:  ERC8004ValidationRegistryV3  [or V1]
         Constructor: (address identityRegistry, address zkVerifier)
         Depends on: Step 2 address, Step 1 address

Step 5:  VaultfireERC8004AdapterV3  (V3.3 — Contract 17)
         Constructor: (address partnershipBonds, address identityRegistry,
                       address reputationRegistry, address validationRegistry)
         Depends on: AIPartnershipBondsV3 (must be deployed separately),
                     Step 2, Step 3, Step 4 addresses

Step 6:  Post-deploy configuration (REQUIRED):
         reputationRegistry.setAuthorizedCaller(adapterAddress, true)
         → Called by the reputationRegistry owner
         → Without this, adapter.syncPartnershipReputation() will revert

Step 7:  Optional but recommended:
         identityRegistry.transferOwnership(multisigAddress)
         reputationRegistry.transferOwnership(multisigAddress)
         adapter.transferOwnership(multisigAddress)
```

---

## 5. Constructor Arguments Reference

| Contract | Constructor Arguments |
|----------|-----------------------|
| `ERC8004IdentityRegistryV3` | `()` — none |
| `ERC8004ReputationRegistryV3` | `(address _identityRegistry)` |
| `ERC8004ValidationRegistryV3` | `(address _identityRegistry, address _zkVerifier)` |
| `VaultfireERC8004AdapterV3` (V3.3) | `(address _partnershipBonds, address _identityRegistry, address _reputationRegistry, address _validationRegistry)` |
| `IERC8004IdentityRegistry` | Not deployable (interface) |

---

## 6. Version Matrix

| Version Tag | Identity | Reputation | Validation | Adapter | Notes |
|------------|---------|------------|------------|---------|-------|
| V1 | `contracts/ERC8004IdentityRegistry.sol` | `contracts/ERC8004ReputationRegistry.sol` | `contracts/ERC8004ValidationRegistry.sol` | `contracts/VaultfireERC8004Adapter.sol` | Original |
| V3-early | `v3/ERC8004IdentityRegistryV3.sol` | `v3/ERC8004ReputationRegistryV3.sol` | `v3/ERC8004ValidationRegistryV3.sol` | — | Pre-PR |
| V3-PR | `v3-pr-ready/.../ERC8004IdentityRegistryV3.sol` | same | same | `v3-pr-ready/.../VaultfireERC8004AdapterV3.sol` (BondsV2) | PR-ready |
| V3.2 | `test_fixes/.../ERC8004ReputationRegistryV3.sol` | `test_fixes/.../ERC8004ValidationRegistryV3.sol` | — | — | test_fixes branch only |
| V3.3 | `vf-audit-contracts/ERC8004IdentityRegistryV3.sol` | same | same | `vf-audit-contracts/VaultfireERC8004AdapterV3.sol` | **PRODUCTION** — all audit round 3 fixes applied |
| theloopbreaker | `theloopbreaker-site/contracts/ERC8004IdentityRegistry.sol` (V1) | — | `theloopbreaker-site/contracts/ERC8004ValidationRegistry.sol` | — | Variant; has underflow bug |
| V3-decoded | — | — | — | `VaultfireERC8004AdapterV3_decoded.sol` | Intermediate; HIGH-14 not fixed |

---

## 7. Hardhat / Foundry Setup

### Hardhat

```javascript
// hardhat.config.js
module.exports = {
  solidity: {
    version: "0.8.25",
    settings: {
      optimizer: { enabled: true, runs: 200 }
    }
  }
};
```

### Foundry

```toml
# foundry.toml
[profile.default]
solc_version = "0.8.25"
optimizer = true
optimizer_runs = 200
```

---

## 8. Test Deployment Script Outline

```javascript
// deploy.js (ethers.js / Hardhat)
const [deployer] = await ethers.getSigners();

// Step 1: ZK Verifier
const Verifier = await ethers.getContractFactory("BeliefAttestationVerifier");
const verifier = await Verifier.deploy(/* verifier-specific args */);

// Step 2: Identity Registry
const Identity = await ethers.getContractFactory("ERC8004IdentityRegistryV3");
const identity = await Identity.deploy();

// Step 3: Reputation Registry
const Reputation = await ethers.getContractFactory("ERC8004ReputationRegistryV3");
const reputation = await Reputation.deploy(identity.address);

// Step 4: Validation Registry
const Validation = await ethers.getContractFactory("ERC8004ValidationRegistryV3");
const validation = await Validation.deploy(identity.address, verifier.address);

// Step 5: Adapter (assumes AIPartnershipBondsV3 already deployed)
const Adapter = await ethers.getContractFactory("VaultfireERC8004AdapterV3");
const adapter = await Adapter.deploy(
  bondsAddress,       // pre-deployed AIPartnershipBondsV3
  identity.address,
  reputation.address,
  validation.address
);

// Step 6: Authorize adapter as caller on reputation registry
await reputation.setAuthorizedCaller(adapter.address, true);

// Step 7: Transfer ownership to multisig
await identity.transferOwnership(multisigAddress);
await reputation.transferOwnership(multisigAddress);
await adapter.transferOwnership(multisigAddress);

console.log("Identity:   ", identity.address);
console.log("Reputation: ", reputation.address);
console.log("Validation: ", validation.address);
console.log("Adapter:    ", adapter.address);
```

---

## 9. Key Storage Slots for Monitoring

Monitoring systems should watch these state transitions:

| Mapping | Key Type | Watch For |
|---------|---------|-----------|
| `bondReputationSynced` | `uint256 bondId` | Set to `false` via reset — signals pending re-sync |
| `bondSyncCount` | `uint256 bondId` | Approaching `MAX_BOND_SYNCS` (3) |
| `bondSyncResetRequestedAt` | `uint256 bondId` | Non-zero value — pending timelock reset |
| `authorizedCallers` | `address caller` | Any new address added |
| `validatorStakes` | `address validator` | Drops to zero (validator near-slashed out) |
| `agentStatus` | `address agent` | Changes from ACTIVE to DEACTIVATED |

---

## 10. Audit History Summary

| Round | Date | Key Findings Fixed |
|-------|------|--------------------|
| Round 1 (HIGH-001) | 2026-02-23 | `ReentrancyGuard` added to Validation Registry; `.transfer()` → `.call{}` |
| Round 2 (Partner-Ready) | 2026-02-xx | CRITICAL-AD-01, HIGH-AD-01–03, MEDIUM-AD-01–02, CRITICAL-VR-01, HIGH-VR-01, HIGH-IR-01–02, HIGH-RR-01, MEDIUM-VR-01–05 |
| Round 3 (V3.3) | 2026-03-xx | HIGH-14 (timelock sync reset), HIGH-16 (bond creation identity check via interface), HIGH-17 (reputation revocation), V3.3 active-count decrement for DISPUTED/CANCELLED |



<div style="page-break-before:always"></div>

# Vaultfire Codex — Batch 4: Belief / Attestation / Oracle / ZK-Proof Contracts

**Comprehensive Line-by-Line Technical Reference**

*40 contracts across DilithiumAttestor, ProductionBeliefAttestationVerifier, BeliefAttestationVerifier (dev), BeliefOracle/BeliefOracleV3, MultiOracleConsensus/V3, FlourishingMetricsOracle/V3, BaseOracle, VaultfireTrustOracle, interfaces IStarkVerifier/IRiscZeroVerifier/IFlourishingMetricsOracle, MockRiscZeroVerifier, Foundry test suites.*

---

## Table of Contents

1. [contracts/IStarkVerifier.sol](#1-contractsistarkverifiersol)
2. [theloopbreaker-site/risc0/interfaces/IStarkVerifier.sol](#2-theloopbreaker-siterisc0interfacesistarkverifiersol)
3. [contracts/IRiscZeroVerifier.sol](#3-contractsirisczeroveriferrsol)
4. [theloopbreaker-site/base-mini-app/contracts/IRiscZeroVerifier.sol](#4-theloopbreaker-sitebase-mini-appcontractsirisczeroversifiersol)
5. [theloopbreaker-site/risc0/interfaces/IRiscZeroVerifier.sol](#5-theloopbreaker-siterisc0interfacesirisczeroveriferrsol)
6. [vf-audit-contracts/IFlourishingMetricsOracle.sol](#6-vf-audit-contractsiflourishingmetricsoraclesol)
7. [contracts/DilithiumAttestor.sol](#7-contractsdilithiumattestorsol)
8. [theloopbreaker-site/base-mini-app/contracts/DilithiumAttestor.sol](#8-theloopbreaker-sitebase-mini-appcontractsdilithiumattestorsol)
9. [vf-audit-contracts/DilithiumAttestorV3.sol](#9-vf-audit-contractsdilithiumattestorv3sol)
10. [v3/DilithiumAttestorV3.sol](#10-v3dilithiumattestorv3sol)
11. [v3-pr-ready/contracts/v3/DilithiumAttestorV3.sol](#11-v3-pr-readycontractsv3dilithiumattestorv3sol)
12. [push_contracts_v3_DilithiumAttestorV3.sol](#12-push_contracts_v3_dilithiumattestorv3sol)
13. [contracts/BeliefAttestationVerifier.sol](#13-contractsbeliefattestationverifiersol)
14. [contracts/BeliefAttestationVerifierProduction.sol](#14-contractsbeliefattestationverifierproductionsol)
15. [contracts/ProductionBeliefAttestationVerifier.sol](#15-contractsproductionbeliefattestationverifiersol)
16. [vf-audit-contracts/ProductionBeliefAttestationVerifierV3.sol](#16-vf-audit-contractsproductionbeliefattestationverifierv3sol)
17. [v3/ProductionBeliefAttestationVerifierV3.sol](#17-v3productionbeliefattestationverifierv3sol)
18. [v3-pr-ready/contracts/v3/ProductionBeliefAttestationVerifierV3.sol](#18-v3-pr-readycontractsv3productionbeliefattestationverifierv3sol)
19. [push_contracts_v3_ProductionBeliefAttestationVerifierV3.sol](#19-push_contracts_v3_productionbeliefattestationverifierv3sol)
20. [theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol](#20-theloopbreaker-siterisc0srcproductionbeliefattestationverifiersol)
21. [contracts/BeliefOracle.sol](#21-contractsbeliefoaclesol)
22. [v3/BeliefOracleV3.sol](#22-v3beliefOraclev3sol)
23. [v3-pr-ready/contracts/v3/BeliefOracleV3.sol](#23-v3-pr-readycontractsv3beliefOraclev3sol)
24. [vf-audit-contracts/BeliefOracleV3.sol](#24-vf-audit-contractsbeliefOraclev3sol)
25. [push_contracts_v3_BeliefOracleV3.sol](#25-push_contracts_v3_beliefOraclev3sol)
26. [contracts/MultiOracleConsensus.sol](#26-contractsmultioracleconsensussol)
27. [theloopbreaker-site/contracts/MultiOracleConsensus.sol](#27-theloopbreaker-sitecontractsmultioracleconsensussol)
28. [vf-audit-contracts/MultiOracleConsensusV3.sol](#28-vf-audit-contractsmultioracleconsensusv3sol)
29. [v3-pr-ready/contracts/v3/MultiOracleConsensusV3.sol](#29-v3-pr-readycontractsv3multioracleconsensusv3sol)
30. [push_contracts_v3_MultiOracleConsensusV3.sol](#30-push_contracts_v3_multioracleconsensusv3sol)
31. [contracts/FlourishingMetricsOracle.sol](#31-contractsflourishingmetricsoraclesol)
32. [theloopbreaker-site/contracts/FlourishingMetricsOracle.sol](#32-theloopbreaker-sitecontractsflourishingmetricsoraclesol)
33. [push_contracts_v3_FlourishingMetricsOracleV3.sol](#33-push_contracts_v3_flourishingmetricsOraclev3sol)
34. [test_fixes/contracts/FlourishingMetricsOracleV3.sol](#34-test_fixescontractsflourishingmetricsOraclev3sol)
35. [vf-audit-contracts/FlourishingMetricsOracleV3.sol](#35-vf-audit-contractsflourishingmetricsOraclev3sol)
36. [v3-pr-ready/contracts/v3/FlourishingMetricsOracleV3.sol](#36-v3-pr-readycontractsv3flourishingmetricsOraclev3sol)
37. [contracts/BaseOracle.sol](#37-contractsbaseoraclesol)
38. [theloopbreaker-site/contracts/VaultfireTrustOracle.sol](#38-theloopbreaker-sitecontractsvaultfiretrustoraclesol)
39. [theloopbreaker-site/contracts/mocks/MockRiscZeroVerifier.sol](#39-theloopbreaker-sitecontractsmocksmockrisczeroverifiersol)
40. [theloopbreaker-site/risc0/test/ProductionBeliefAttestationVerifier.t.sol](#40-theloopbreaker-siterisc0testproductionbeliefattestationverifiertSol)

---

## Global Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                   VAULTFIRE ZK BELIEF STACK                     │
├─────────────────────────────────────────────────────────────────┤
│  GUEST PROGRAM (Rust / RISC Zero zkVM)                          │
│  ─ Validates belief criteria off-chain                          │
│  ─ Commits journal: (beliefHash, attester, epoch, moduleId,     │
│      beliefScore, timestamp)                                    │
│  ─ Boundless prover wraps execution trace in Groth16 SNARK      │
├─────────────────────────────────────────────────────────────────┤
│  ON-CHAIN VERIFIERS                                             │
│  ┌──────────────────────┐  ┌──────────────────────┐            │
│  │ BeliefAttestationVer │  │ ProductionBeliefAtt-  │            │
│  │ ifier (DEV, 31337)   │  │ estationVerifier(V2/3)│            │
│  └──────────┬───────────┘  └──────────┬────────────┘            │
│             └────────────┬────────────┘                         │
│                          ▼                                      │
│            IStarkVerifier / IRiscZeroVerifier                   │
│                          │                                      │
│                    RiscZeroVerifierRouter                        │
│           (Base Mainnet: 0x0b144e07a0826182b6b59788c34b32bfa86fb711) │
├─────────────────────────────────────────────────────────────────┤
│  BELIEF ATTESTORS                                               │
│  ┌─────────────────────┐  ┌──────────────────────┐             │
│  │ DilithiumAttestor   │  │ DilithiumAttestorV3   │             │
│  │ (V1, global mapping)│  │ (per-user, ECDSA bind)│             │
│  └──────────┬──────────┘  └──────────┬────────────┘             │
├─────────────────────────────────────────────────────────────────┤
│  BELIEF ORACLE GATEWAY                                          │
│  ┌─────────────────────┐  ┌──────────────────────┐             │
│  │ BeliefOracle (V1)   │  │ BeliefOracleV3        │             │
│  │ resonance + bonus   │  │ + MIN_BELIEF_COUNT    │             │
│  └──────────┬──────────┘  └──────────┬────────────┘             │
│             └────────────┬────────────┘                         │
│                          ▼                                      │
│                   RewardStream                                  │
├─────────────────────────────────────────────────────────────────┤
│  ORACLE CONSENSUS LAYERS                                        │
│  MultiOracleConsensus (V1)  →  MultiOracleConsensusV3           │
│  FlourishingMetricsOracle   →  FlourishingMetricsOracleV3       │
│  BaseOracle (CID/zkHash pin)                                    │
│  VaultfireTrustOracle (off-chain performance attestations)      │
└─────────────────────────────────────────────────────────────────┘
```

**Journal format (V3 standard):**

```
abi.encode(
    bytes32 beliefHash,   // keccak256 of the belief content
    address attester,     // EOA submitting the proof
    uint32  epoch,        // protocol epoch
    uint32  moduleId,     // belief module identifier
    uint32  beliefScore,  // 0–10000 bps, must be ≥8000 to pass threshold
    uint64  timestamp     // block.timestamp at proof generation
)
journalDigest = sha256(journal)
```

**Attestation key formula:**
```
key = keccak256(abi.encodePacked(beliefHash, attester, epoch))
```

**ECDSA digest (V3 DilithiumAttestorV3):**
```
digest = keccak256(abi.encodePacked(beliefHash, msg.sender)).toEthSignedMessageHash()
```

---

## 1. contracts/IStarkVerifier.sol

**Path:** `/home/user/workspace/contracts/IStarkVerifier.sol`
**Role:** Interface — Stark/RiscZero proof verifier abstraction

### Summary

`IStarkVerifier` is the canonical interface that all Vaultfire belief verifier contracts implement. It defines three functions: `verifyProof` (the primary proof verification entry point accepting a raw proof and a `uint256[]` public inputs array), `getPublicInputsCount` (returns the expected length of that array), and `getProofSystemId` (returns a human-readable identifier string for the proof system). This interface decouples the attestor contracts (`DilithiumAttestor`, `DilithiumAttestorV3`) from any specific proof backend — an operator can swap between a STARK verifier, a Groth16 wrapper, or a RiscZero verifier router as long as it satisfies this interface.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  interface IStarkVerifier {
 5      /// @notice Verify a zero-knowledge proof.
 6      /// @param proofBytes The serialized proof bytes.
 7      /// @param publicInputs The public inputs to the proof.
 8      /// @return True if the proof is valid, false otherwise.
 9      function verifyProof(
10          bytes calldata proofBytes,
11          uint256[] calldata publicInputs
12      ) external returns (bool);
13
14      /// @notice Returns the number of public inputs expected by this verifier.
15      function getPublicInputsCount() external view returns (uint256);
16
17      /// @notice Returns the proof system identifier.
18      function getProofSystemId() external view returns (string memory);
19  }
```

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 1 | SPDX-License-Identifier MIT — open source, permissive. |
| 2 | `pragma solidity ^0.8.25` — targets the 0.8.25 release train; overflow protection is built-in. |
| 4–18 | `interface IStarkVerifier` — pure ABI declaration, no storage, no constructor. |
| 9–12 | `verifyProof(bytes, uint256[])` — `external` (callable from outside), returns `bool`. Note: `external` on an interface function means any implementer exposes this to other contracts. The `calldata` location for both arguments avoids unnecessary memory copies. |
| 15 | `getPublicInputsCount()` — `view`, no state change. Lets callers pre-validate array length before calling `verifyProof`. |
| 18 | `getProofSystemId()` — returns a `string memory` identifier (e.g. `"RISC0-STARK-BeliefAttestation-Production-v2.0"`). Used for upgrade-safety checks and off-chain tooling. |

### State Variables

None — interface only.

### Functions

| Function | Mutability | Returns | Purpose |
|----------|------------|---------|---------|
| `verifyProof(bytes, uint256[])` | state-changing (non-view) | `bool` | Submit proof + public inputs; implementer writes attestation state on success. |
| `getPublicInputsCount()` | view | `uint256` | Returns expected length of the `publicInputs` array. V1 = 4; V3 audit = 6. |
| `getProofSystemId()` | view | `string memory` | Returns proof system string for off-chain routing and version checks. |

### Math

No numeric computation in the interface itself.

### Events

None.

### Security Invariants

- Callers must pre-validate `publicInputs.length == getPublicInputsCount()` to avoid reverts or silent mismatch.
- `verifyProof` is non-view despite returning bool, because implementations write attestation records on success.

### ZK Proof Flow

Not applicable — interface only. See implementing contracts (§14–§19) for flow diagrams.

### Dependencies + Rebuild Recipe

No imports. Compile with:
```bash
forge build --contracts contracts/IStarkVerifier.sol
```

---

## 2. theloopbreaker-site/risc0/interfaces/IStarkVerifier.sol

**Path:** `/home/user/workspace/theloopbreaker-site/risc0/interfaces/IStarkVerifier.sol`
**Role:** Interface — Duplicate of contracts/IStarkVerifier.sol in the risc0 sub-package

### Summary

Functionally identical to contract §1. The risc0 sub-package maintains its own copy of `IStarkVerifier` to allow the risc0 source tree to compile independently without circular import dependencies into the root `contracts/` folder. The three function signatures (`verifyProof`, `getPublicInputsCount`, `getProofSystemId`) are byte-for-byte identical to §1.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  interface IStarkVerifier {
 5      function verifyProof(
 6          bytes calldata proofBytes,
 7          uint256[] calldata publicInputs
 8      ) external returns (bool);
 9
10      function getPublicInputsCount() external view returns (uint256);
11
12      function getProofSystemId() external view returns (string memory);
13  }
```

### Line-by-Line Walkthrough

Identical to §1. No differences.

### State Variables / Functions / Events / Security Invariants

Same as §1.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/risc0/interfaces/IStarkVerifier.sol
```

---

## 3. contracts/IRiscZeroVerifier.sol

**Path:** `/home/user/workspace/contracts/IRiscZeroVerifier.sol`
**Role:** Interface — RiscZero proof verifier (root contracts package, returns bool)

### Summary

`IRiscZeroVerifier` is the local (root) package interface for the RISC Zero on-chain verifier. It exposes two functions: `verify(proof, imageId, journalDigest) returns (bool)` and `version() returns (string)`. The `verify` function takes a serialized proof (`bytes`), the expected image ID (`bytes32`, a hash of the compiled guest ELF binary), and the journal digest (`bytes32`, computed as `sha256(journalBytes)`), and returns `true` on success. Notably this interface uses a **boolean return** rather than the revert-on-failure semantics seen in the Apache-licensed interface at §5.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  interface IRiscZeroVerifier {
 5      /// @notice Verify a RISC Zero proof.
 6      /// @param proof The serialized proof bytes (Groth16 seal).
 7      /// @param imageId The expected image ID (hash of the compiled guest program).
 8      /// @param journalDigest SHA-256 digest of the journal committed by the guest.
 9      /// @return True if the proof is valid, false otherwise.
10      function verify(
11          bytes calldata proof,
12          bytes32 imageId,
13          bytes32 journalDigest
14      ) external returns (bool);
15
16      /// @notice Returns the verifier version string.
17      function version() external view returns (string memory);
18  }
```

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 10–14 | `verify` — accepts three arguments. `proof` is the Groth16 SNARK seal bytes from a RISC Zero receipt. `imageId` is compared against what the verifier expects (anchors the proof to a specific guest binary). `journalDigest = sha256(abi.encode(...))` binds the proof to specific public outputs. Returns `bool` — callers must check return value and handle `false` paths. |
| 17 | `version()` — version string for upgrade routing and debugging. |

### Math

Journal digest: \(\text{journalDigest} = \text{SHA-256}(\text{journalBytes})\)

The SHA-256 is computed in Solidity using the built-in `sha256()` precompile (address `0x02`). This is distinct from `keccak256` — RISC Zero uses SHA-256 for compatibility with the off-chain Rust guest.

### Security Invariants

- Callers using this interface version must explicitly handle a `false` return — silent acceptance of a `false` proof is a critical vulnerability.
- The `imageId` parameter pins the verification to a specific guest program binary. Passing the wrong `imageId` causes all proofs to return `false`.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/IRiscZeroVerifier.sol
```

---

## 4. theloopbreaker-site/base-mini-app/contracts/IRiscZeroVerifier.sol

**Path:** `/home/user/workspace/theloopbreaker-site/base-mini-app/contracts/IRiscZeroVerifier.sol`
**Role:** Interface — RiscZero verifier for the Base mini-app sub-package (^0.8.20, returns bool)

### Summary

Mini-app local copy of `IRiscZeroVerifier`. Uses `pragma solidity ^0.8.20` (older than the root 0.8.25 but compatible). The `verify` function signature is identical to §3 — returns `bool`. Used by `DilithiumAttestor.sol` in the base-mini-app sub-tree to call the deployed RISC Zero verifier router on Base Mainnet.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.20;
 3
 4  interface IRiscZeroVerifier {
 5      function verify(
 6          bytes calldata proof,
 7          bytes32 imageId,
 8          bytes32 journalDigest
 9      ) external returns (bool);
10
11      function version() external view returns (string memory);
12  }
```

### Line-by-Line Walkthrough

Line 2: `^0.8.20` — allows any compiler from 0.8.20 to <0.9.0. This is slightly older than the root package but all semantics are equivalent.

All other lines are functionally identical to §3.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/base-mini-app/contracts/IRiscZeroVerifier.sol \
  --solc-version 0.8.20
```

---

## 5. theloopbreaker-site/risc0/interfaces/IRiscZeroVerifier.sol

**Path:** `/home/user/workspace/theloopbreaker-site/risc0/interfaces/IRiscZeroVerifier.sol`
**Role:** Interface — Canonical Apache-2.0 RISC Zero verifier interface (revert-on-failure semantics)

### Summary

This is the **Apache-2.0 licensed** RISC Zero interface — the upstream canonical definition used by the RISC Zero team. It differs critically from the MIT versions at §3 and §4: `verify` does **not** return a boolean; instead it **reverts on proof failure**. It also includes the `verifyIntegrity(Receipt calldata)` function for full receipt verification, and defines the `Receipt` struct containing both the journal and the seal. The Base Mainnet `RiscZeroVerifierRouter` at `0x0b144e07a0826182b6b59788c34b32bfa86fb711` implements this interface.

### Full Source

```solidity
 1  // SPDX-License-Identifier: Apache-2.0
 2  pragma solidity ^0.8.25;
 3
 3  // Upstream RISC Zero Ethereum interface
 4  // Base Mainnet: 0x0b144e07a0826182b6b59788c34b32bfa86fb711
 5
 6  struct Receipt {
 7      bytes seal;
 8      bytes journal;
 9  }
 10
 11 interface IRiscZeroVerifier {
 12     /// @notice Verifies a RISC Zero receipt.
 13     /// @dev REVERTS on invalid proof. Does NOT return bool.
 14     function verify(
 15         bytes calldata seal,
 16         bytes32 imageId,
 17         bytes32 journalDigest
 18     ) external;
 19
 20     /// @notice Verifies a full Receipt (seal + journal).
 21     function verifyIntegrity(Receipt calldata receipt) external view;
 22
 23     /// @notice Returns the verifier version string.
 24     function version() external view returns (string memory);
 25 }
```

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 1 | Apache-2.0 license — upstream RISC Zero code. |
| 6–9 | `Receipt` struct — packages the Groth16 `seal` (proof bytes) and `journal` (raw output bytes committed by the guest) into a single struct for `verifyIntegrity`. |
| 14–18 | `verify(seal, imageId, journalDigest)` — **no return value**; reverts on any verification failure. Callers must use `try/catch` to handle failures without bubbling the revert. This is the interface implemented by `ProductionBeliefAttestationVerifier` (§20) via `try riscZeroVerifier.verify(...)`. |
| 21 | `verifyIntegrity(Receipt)` — verifies both the seal and matches `sha256(receipt.journal)` against the journal digest. Marked `view` — no state writes. |

### Math

\[\text{journalDigest} = \text{SHA-256}(\text{receipt.journal})\]

The verifier computes this digest internally and compares it against what the guest committed.

### Security Invariants

- **Critical:** This interface uses revert-on-failure, not boolean returns. Any wrapper that treats the `bool` return (from §3/§4 signatures) as though it came from this interface will experience a type mismatch or silent incorrect behavior.
- `verifyIntegrity` is `view` — safe to call in view contexts for integrity checks without gas-intensive state writes.

### ZK Proof Flow (Canonical RISC Zero)

```
Off-chain:
  Rust guest executes inside zkVM → commits journal
  Boundless/local prover generates Groth16 SNARK (seal)

On-chain:
  Caller → verify(seal, imageId, sha256(journal))
         → RiscZeroVerifierRouter (0x0b144e...)
              ├─ Routes by proof type prefix in seal
              ├─ Delegates to registered verifier for that type
              └─ Reverts if invalid; returns normally if valid
```

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/risc0/interfaces/IRiscZeroVerifier.sol
```

---

## 6. vf-audit-contracts/IFlourishingMetricsOracle.sol

**Path:** `/home/user/workspace/vf-audit-contracts/IFlourishingMetricsOracle.sol`
**Role:** Interface — FlourishingMetricsOracle consumer API

### Summary

`IFlourishingMetricsOracle` defines the two query functions that oracle consumers (e.g., DAO contracts, vault systems) call to read metrics: `getMetricTWAP(metricId, window)` returns the time-weighted average price for a metric, and `isMetricFresh(metricId)` returns whether the most recent update falls within the oracle's freshness window. This interface is implemented by all `FlourishingMetricsOracleV3` variants (§33–§36).

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  interface IFlourishingMetricsOracle {
 5      /// @notice Returns the Time-Weighted Average Price for a metric.
 6      /// @param metricId The metric identifier (bytes32).
 7      /// @param window   The TWAP lookback window in seconds (may be ignored by V3).
 8      function getMetricTWAP(bytes32 metricId, uint256 window)
 9          external view returns (uint256);
 10
 11     /// @notice Returns true if the metric was updated within the freshness window.
 12     /// @param metricId The metric identifier.
 13     function isMetricFresh(bytes32 metricId)
 14         external view returns (bool);
 15 }
```

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 8–9 | `getMetricTWAP(bytes32, uint256)` — `view`, returns `uint256`. The `window` parameter is accepted for interface compatibility but is ignored by the V3 EMA-based implementation (which uses a fixed EMA coefficient of 3:1). |
| 13–14 | `isMetricFresh(bytes32)` — `view`, returns `bool`. V3 implementation checks `block.timestamp - lastUpdated ≤ MAX_METRIC_AGE` where `MAX_METRIC_AGE = 1 hours`. |

### Math

TWAP formula (EMA, implemented in FlourishingMetricsOracleV3):
\[\text{TWAP}_n = \begin{cases} v_0 & \text{if first update} \\ \frac{3 \cdot \text{TWAP}_{n-1} + v_n}{4} & \text{otherwise} \end{cases}\]

where \(v_n\) is the new value (or weighted mean in V3 vf-audit variant).

### Security Invariants

- Consumers must check `isMetricFresh` before trusting `getMetricTWAP` to avoid acting on stale data.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts vf-audit-contracts/IFlourishingMetricsOracle.sol
```

---

## 7. contracts/DilithiumAttestor.sol

**Path:** `/home/user/workspace/contracts/DilithiumAttestor.sol`
**Role:** Belief verifier — V1 Dilithium attestor, global mapping (vulnerable to cross-user replay)

### Summary

`DilithiumAttestor` is the V1 on-chain attestation contract. It receives a `(beliefHash, proof, signature)` tuple from a caller, validates the ECDSA signature against a signer address, constructs a 4-element `publicInputs` array, and calls the injected `IStarkVerifier` to verify the ZK proof. On success it records `attestedBeliefs[beliefHash] = true`. The critical vulnerability in V1 is that `attestedBeliefs` is a **global mapping** — once any user attests a belief hash, any subsequent attester for the same hash gets a false positive from `hasAttested()` even without submitting their own proof. This is the HIGH-1 finding fixed in V3.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import {IStarkVerifier} from "./IStarkVerifier.sol";
 5  import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
 6
 7  contract DilithiumAttestor {
 8      using ECDSA for bytes32;
 9
10      IStarkVerifier public immutable verifier;
11      address public immutable signer;
12
13      // V1 BUG: global mapping — beliefHash → attested (not per-user)
14      mapping(bytes32 => bool) public attestedBeliefs;
15
15      event BeliefAttested(
16          address indexed attester,
17          bytes32 indexed beliefHash,
18          uint256 timestamp
19      );
20
21      error AlreadyAttested();
22      error InvalidSignature();
23      error ProofVerificationFailed();
24      error EmptyProof();
25
26      constructor(address verifier_, address signer_) {
27          require(verifier_ != address(0), "zero verifier");
28          require(signer_ != address(0), "zero signer");
29          verifier = IStarkVerifier(verifier_);
30          signer = signer_;
31      }
32
33      function attestBelief(
34          bytes32 beliefHash,
35          bytes calldata proof
36      ) external {
37          if (attestedBeliefs[beliefHash]) revert AlreadyAttested();
38          if (proof.length == 0) revert EmptyProof();
39
39          // ECDSA: verify that `signer` authorized this beliefHash
40          bytes32 digest = keccak256(abi.encodePacked(beliefHash))
41              .toEthSignedMessageHash();
42          address recovered = digest.recover(proof[0:65]);
43          if (recovered != signer) revert InvalidSignature();
44
44          // Build publicInputs for the ZK verifier
45          uint256[] memory publicInputs = new uint256[](4);
46          publicInputs[0] = uint256(beliefHash);
47          publicInputs[1] = uint256(uint160(msg.sender));
48          publicInputs[2] = 0; // epoch placeholder
49          publicInputs[3] = 0; // moduleID placeholder
50
51          bytes memory zkProof = proof[65:];
52          bool ok = verifier.verifyProof(zkProof, publicInputs);
53          if (!ok) revert ProofVerificationFailed();
54
55          attestedBeliefs[beliefHash] = true;
56          emit BeliefAttested(msg.sender, beliefHash, block.timestamp);
57      }
58
59      function hasAttested(bytes32 beliefHash) external view returns (bool) {
60          return attestedBeliefs[beliefHash];
61      }
62  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 4 | Imports `IStarkVerifier` — the proof verification interface. |
| 5 | Imports OpenZeppelin `ECDSA` for `toEthSignedMessageHash()` and `recover()`. |
| 10 | `verifier` — immutable; set once in constructor, cannot be changed. Prevents verifier swap attacks. |
| 11 | `signer` — immutable authorized ECDSA signer address. Only attestations signed by this key pass. |
| 14 | **`attestedBeliefs[bytes32] => bool`** — global mapping. HIGH-1 vulnerability: attesting `beliefHash` once locks it for everyone. |
| 37 | Guard: `if (attestedBeliefs[beliefHash]) revert AlreadyAttested()` — once set globally, all subsequent callers revert even with valid proofs. |
| 40–43 | ECDSA digest construction: `keccak256(abi.encodePacked(beliefHash))` — note this digest does NOT include `msg.sender`. This is the HIGH-2 finding: any holder of a signed `beliefHash` can re-use it from a different address. |
| 45–49 | 4-element `publicInputs`: `[beliefHash, msg.sender, 0, 0]`. Epoch and moduleID are hardcoded to 0. |
| 51 | `proof[65:]` — the first 65 bytes of `proof` are the ECDSA signature; the remainder is the ZK proof body. |
| 52–53 | `verifier.verifyProof(zkProof, publicInputs)` — delegates to the injected verifier. Returns bool; reverts on false. |
| 55 | CEI violation: state is written AFTER external call. Mitigated by guard at line 37, but still a structural reentrancy risk. |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `verifier` | `IStarkVerifier` | immutable | ZK proof verifier backend |
| `signer` | `address` | immutable | Authorized ECDSA signer |
| `attestedBeliefs` | `mapping(bytes32 => bool)` | mutable | Global attestation registry (V1 vulnerability) |

### Functions

**`constructor(address verifier_, address signer_)`**
- Validates non-zero addresses.
- Sets `verifier` and `signer` as immutable values.

**`attestBelief(bytes32 beliefHash, bytes calldata proof) external`**
- Guards: `AlreadyAttested` (global), `EmptyProof`.
- Extracts ECDSA signature from `proof[0:65]`, verifies against `signer`.
- Constructs 4-element `publicInputs`, calls `verifier.verifyProof`.
- Sets `attestedBeliefs[beliefHash] = true`.
- Emits `BeliefAttested`.

**`hasAttested(bytes32 beliefHash) external view returns (bool)`**
- Returns global attestation flag. Does not differentiate by user.

### Math

ECDSA digest:
\[\text{digest} = \text{EthSignedMessageHash}(\text{keccak256}(\text{beliefHash}))\]
where EthSignedMessageHash prepends `"\x19Ethereum Signed Message:\n32"`.

Public inputs array:
\[\text{publicInputs} = [\text{uint256(beliefHash)},\; \text{uint160(msg.sender)},\; 0,\; 0]\]

### Events

| Event | Trigger | Fields |
|-------|---------|--------|
| `BeliefAttested(address attester, bytes32 beliefHash, uint256 timestamp)` | Successful proof verification | `attester` (indexed), `beliefHash` (indexed), `block.timestamp` |

### Security Invariants

1. **HIGH-1 (unfixed):** `attestedBeliefs` is global. Once any user attests a beliefHash, it is permanently locked and subsequent valid attesters get `AlreadyAttested`.
2. **HIGH-2 (unfixed):** ECDSA digest does not bind `msg.sender`, enabling signature replay from different wallets.
3. **CEI violation:** `attestedBeliefs[beliefHash] = true` is set after the external `verifier.verifyProof` call. A malicious verifier could re-enter.

### ZK Proof Flow

```
Caller submits: attestBelief(beliefHash, proof)
  proof = [65 bytes ECDSA sig] ++ [N bytes ZK proof]

Step 1: recover ECDSA signer from proof[0:65]
        digest = keccak256(abi.encodePacked(beliefHash)).toEthSignedMessageHash()
        recovered = ECDSA.recover(digest, proof[0:65])
        assert recovered == signer

Step 2: publicInputs = [beliefHash, msg.sender, 0, 0]
        zkProof = proof[65:]
        ok = verifier.verifyProof(zkProof, publicInputs)
        assert ok == true

Step 3: attestedBeliefs[beliefHash] = true
        emit BeliefAttested(msg.sender, beliefHash, block.timestamp)
```

### Dependencies + Rebuild Recipe

```
contracts/
├── IStarkVerifier.sol
└── DilithiumAttestor.sol
@openzeppelin/contracts/utils/cryptography/ECDSA.sol
```

```bash
forge build
```

---

## 8. theloopbreaker-site/base-mini-app/contracts/DilithiumAttestor.sol

**Path:** `/home/user/workspace/theloopbreaker-site/base-mini-app/contracts/DilithiumAttestor.sol`
**Role:** Belief verifier — Base mini-app variant, uses IRiscZeroVerifier directly, mutable zkEnabled flag, per-user timestamp mapping

### Summary

The mini-app variant of `DilithiumAttestor` targets the Base Mainnet deployment of the RISC Zero verifier router. Unlike the root V1 (§7) it uses `IRiscZeroVerifier` directly (not `IStarkVerifier`), computes a `sha256` journal digest inline, and stores per-user attestation timestamps in `userAttestations[address][bytes32] => uint256`. It also has a mutable `zkEnabled` flag that the owner can toggle — when disabled, proofs are accepted without ZK verification. The per-user mapping is a partial fix for the global-mapping vulnerability, but the `zkEnabled` escape hatch is a significant centralization risk.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.20;
 3
 4  import {IRiscZeroVerifier} from "./IRiscZeroVerifier.sol";
 5  import "@openzeppelin/contracts/access/Ownable.sol";
 6
 7  contract DilithiumAttestor is Ownable {
 8      IRiscZeroVerifier public immutable riscZeroVerifier;
 9      bytes32 public imageId;
 10     bool public zkEnabled;
 11
 12     // Per-user attestation timestamp: user → beliefHash → timestamp
 13     mapping(address => mapping(bytes32 => uint256)) public userAttestations;
 14
 14     event BeliefAttested(address indexed user, bytes32 indexed beliefHash, uint256 timestamp);
 15     event ZKToggled(bool enabled);
 16     event ImageIdUpdated(bytes32 oldId, bytes32 newId);
 17
 18     error AlreadyAttested();
 19     error ZKVerificationFailed();
 20
 20     constructor(
 21         address _riscZeroVerifier,
 22         bytes32 _imageId,
 23         bool _zkEnabled
 24     ) Ownable(msg.sender) {
 25         riscZeroVerifier = IRiscZeroVerifier(_riscZeroVerifier);
 26         imageId = _imageId;
 27         zkEnabled = _zkEnabled;
 28     }
 29
 30     function setZkEnabled(bool enabled) external onlyOwner {
 31         zkEnabled = enabled;
 32         emit ZKToggled(enabled);
 33     }
 34
 35     function updateImageId(bytes32 newImageId) external onlyOwner {
 36         emit ImageIdUpdated(imageId, newImageId);
 37         imageId = newImageId;
 38     }
 39
 40     function attestBelief(
 41         bytes32 beliefHash,
 42         bytes calldata proof
 43     ) external {
 44         if (userAttestations[msg.sender][beliefHash] != 0) revert AlreadyAttested();
 45
 45         if (zkEnabled) {
 46             // Journal: encode beliefHash + msg.sender
 47             bytes memory journal = abi.encode(beliefHash, msg.sender);
 47             bytes32 journalDigest = sha256(journal);
 48             bool ok = riscZeroVerifier.verify(proof, imageId, journalDigest);
 49             if (!ok) revert ZKVerificationFailed();
 49         }
 50
 51         userAttestations[msg.sender][beliefHash] = block.timestamp;
 52         emit BeliefAttested(msg.sender, beliefHash, block.timestamp);
 53     }
 54
 55     function hasAttested(address user, bytes32 beliefHash) external view returns (bool) {
 56         return userAttestations[user][beliefHash] != 0;
 57     }
 58
 59     function getAttestationTime(address user, bytes32 beliefHash)
 60         external view returns (uint256)
 61     {
 62         return userAttestations[user][beliefHash];
 63     }
 64  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 2 | `^0.8.20` — older pragma for mini-app compatibility. |
| 8 | `riscZeroVerifier` — immutable; targets the RiscZero router at deployment. |
| 9 | `imageId` — **mutable** (not immutable). Owner can update without timelock — centralization risk. |
| 10 | `zkEnabled` — mutable flag. When `false`, the proof is never verified — all attestations succeed trivially. |
| 13 | `userAttestations[address][bytes32] => uint256` — per-user fix for the global-mapping HIGH-1 vulnerability. Stores `block.timestamp` rather than `bool`. |
| 30–33 | `setZkEnabled` — owner can disable ZK verification entirely. This is a critical privilege: a compromised owner key can disable all security. |
| 44 | Guard uses `!= 0` (timestamp check) rather than a boolean. A user who attested at exactly timestamp 0 (never, in practice, since block.timestamp > 0) would be treated as unattested — an edge case that is harmless in practice. |
| 46–48 | Journal = `abi.encode(beliefHash, msg.sender)` — only 2 fields, simpler than the V3 6-field format. The journal digest is computed on-chain as `sha256(journal)`. |
| 48 | `riscZeroVerifier.verify(proof, imageId, journalDigest)` — uses `IRiscZeroVerifier` (§4) which returns `bool`. |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `riscZeroVerifier` | `IRiscZeroVerifier` | immutable | RISC Zero verifier router |
| `imageId` | `bytes32` | mutable (owner) | Guest program ELF hash |
| `zkEnabled` | `bool` | mutable (owner) | ZK proof verification toggle |
| `userAttestations` | `mapping(address => mapping(bytes32 => uint256))` | mutable | Per-user attestation timestamp registry |

### Math

Journal digest:
\[\text{journalDigest} = \text{SHA-256}(\text{abi.encode}(\text{beliefHash},\, \text{msg.sender}))\]

### Events

| Event | When | Fields |
|-------|------|--------|
| `BeliefAttested(address user, bytes32 beliefHash, uint256 timestamp)` | Successful attestation | All indexed |
| `ZKToggled(bool enabled)` | Owner toggles `zkEnabled` | `enabled` |
| `ImageIdUpdated(bytes32 oldId, bytes32 newId)` | Owner updates `imageId` | Old and new values |

### Security Invariants

1. `zkEnabled = false` is a **critical bypass** — all attestations succeed with empty proof bytes.
2. `imageId` mutability without timelock is a centralization risk — instant guest program swap.
3. Per-user mapping correctly prevents cross-user replay (partial fix vs V1).

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/base-mini-app/contracts/DilithiumAttestor.sol \
  --solc-version 0.8.20
```

---

## 9. vf-audit-contracts/DilithiumAttestorV3.sol

**Path:** `/home/user/workspace/vf-audit-contracts/DilithiumAttestorV3.sol`
**Role:** Belief verifier — Full V3 audit, per-user mapping, ECDSA msg.sender binding, 6-element publicInputs, CEI fix

### Summary

`DilithiumAttestorV3` is the audited V3 production attestor. It fixes all critical and high findings from the V1 audit: (1) `attestedBeliefs` is replaced with `attestedBeliefs[bytes32][address] => bool` per-user mapping eliminating cross-user replay; (2) the ECDSA digest binds `msg.sender` — `keccak256(abi.encodePacked(beliefHash, msg.sender)).toEthSignedMessageHash()` — preventing signature portability across wallets; (3) the `publicInputs` array is expanded to 6 elements including `beliefScore = 8000` and `block.timestamp`, aligning with the V3 journal format; (4) the state write is moved before the external call (CEI compliance). This is the authoritative V3 attestor implementation.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import {IStarkVerifier} from "./IStarkVerifier.sol";
 5  import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
 6
 7  /// @title DilithiumAttestorV3
 8  /// @notice V3 belief attestor with per-user mapping, msg.sender ECDSA binding,
 9  ///         6-element publicInputs (beliefScore=8000, timestamp), and CEI fix.
10  contract DilithiumAttestorV3 {
11      using ECDSA for bytes32;
12
13      IStarkVerifier public immutable verifier;
14      address public immutable signer;
15
15      /// @dev HIGH-1 FIX: per-user mapping prevents cross-user replay
16      mapping(bytes32 => mapping(address => bool)) public attestedBeliefs;
17
18      event BeliefAttested(
19          address indexed attester,
20          bytes32 indexed beliefHash,
21          uint256 timestamp
22      );
23
24      error AlreadyAttested();
25      error InvalidSignature();
26      error ProofVerificationFailed();
27      error EmptyProof();
28
29      constructor(address verifier_, address signer_) {
30          require(verifier_ != address(0), "zero verifier");
31          require(signer_ != address(0), "zero signer");
32          verifier = IStarkVerifier(verifier_);
33          signer = signer_;
34      }
35
36      function attestBelief(
37          bytes32 beliefHash,
38          bytes calldata proof
39      ) external {
40          // HIGH-1 FIX: per-user attestation guard
41          if (attestedBeliefs[beliefHash][msg.sender]) revert AlreadyAttested();
42          if (proof.length == 0) revert EmptyProof();
43
44          // HIGH-2 FIX: bind ECDSA digest to msg.sender
45          bytes32 digest = keccak256(
46              abi.encodePacked(beliefHash, msg.sender)
47          ).toEthSignedMessageHash();
48          address recovered = digest.recover(proof[0:65]);
49          if (recovered != signer) revert InvalidSignature();
50
51          // CRIT-07 FIX: 6-element publicInputs including beliefScore and timestamp
52          uint256[] memory publicInputs = new uint256[](6);
53          publicInputs[0] = uint256(beliefHash);
54          publicInputs[1] = uint256(uint160(msg.sender));
55          publicInputs[2] = 0; // epoch
56          publicInputs[3] = 0; // moduleID
57          publicInputs[4] = 8000; // beliefScore (minimum threshold in bps)
58          publicInputs[5] = block.timestamp;
59
60          // CEI FIX: write state BEFORE external call
61          attestedBeliefs[beliefHash][msg.sender] = true;
62
63          bytes memory zkProof = proof[65:];
64          bool ok = verifier.verifyProof(zkProof, publicInputs);
65          if (!ok) revert ProofVerificationFailed();
66
67          emit BeliefAttested(msg.sender, beliefHash, block.timestamp);
68      }
69
70      function hasAttested(bytes32 beliefHash, address user)
71          external view returns (bool)
72      {
73          return attestedBeliefs[beliefHash][user];
74      }
75  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 16 | `attestedBeliefs[bytes32][address] => bool` — **HIGH-1 FIX**. Each `(beliefHash, attester)` pair has its own boolean. User A attesting beliefHash X does not block user B. |
| 41 | Per-user guard: `attestedBeliefs[beliefHash][msg.sender]` — checks only the caller's slot. |
| 45–47 | **HIGH-2 FIX:** ECDSA digest = `keccak256(abi.encodePacked(beliefHash, msg.sender))`. The signer must sign `(beliefHash ++ msg.sender)` — the signature is cryptographically bound to the specific wallet. |
| 52–58 | **CRIT-07 FIX:** 6-element `publicInputs`: `[beliefHash, msg.sender, epoch=0, moduleID=0, beliefScore=8000, block.timestamp]`. This matches the V3 journal ABI encoding format expected by the verifier. |
| 61 | **CEI FIX:** `attestedBeliefs[beliefHash][msg.sender] = true` written BEFORE `verifier.verifyProof` external call. If the verifier re-enters, the guard at line 41 prevents double-entry. |
| 64–65 | `verifyProof` called with the ZK proof bytes (after stripping 65-byte ECDSA sig). Reverts on failure. |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `verifier` | `IStarkVerifier` | immutable | ZK proof verifier backend |
| `signer` | `address` | immutable | Authorized ECDSA signer |
| `attestedBeliefs` | `mapping(bytes32 => mapping(address => bool))` | mutable | Per-user attestation registry (HIGH-1 fix) |

### Functions

**`attestBelief(bytes32 beliefHash, bytes calldata proof) external`**
- Per-user guard (HIGH-1 fix).
- ECDSA signer verification with `msg.sender` binding (HIGH-2 fix).
- 6-element `publicInputs` construction (CRIT-07 fix).
- CEI-compliant state write before external call.
- Emits `BeliefAttested`.

**`hasAttested(bytes32 beliefHash, address user) external view returns (bool)`**
- Returns per-user attestation flag.

### Math

ECDSA digest (HIGH-2 fix):
\[\text{digest} = \text{EthSignedMessageHash}(\text{keccak256}(\text{beliefHash} \| \text{msg.sender}))\]

Public inputs:
\[\text{publicInputs} = [\text{beliefHash},\; \text{msg.sender},\; 0,\; 0,\; 8000,\; \text{block.timestamp}]\]

### Events

| Event | Trigger | Fields |
|-------|---------|--------|
| `BeliefAttested(address attester, bytes32 beliefHash, uint256 timestamp)` | Successful attestation | All indexed where applicable |

### Security Invariants

1. Per-user mapping prevents global lockout (HIGH-1 fixed).
2. ECDSA digest includes `msg.sender` — no signature portability (HIGH-2 fixed).
3. CEI compliance: state written before external call (reentrancy risk mitigated).
4. `beliefScore = 8000` hardcoded — the guest program's belief threshold is enforced at the verifier level, not just at the oracle level.

### ZK Proof Flow

```
Caller: attestBelief(beliefHash, [65-byte ECDSA sig] ++ [ZK proof bytes])

Step 1 — ECDSA Verification:
  digest = keccak256(beliefHash ++ msg.sender).toEthSignedMessageHash()
  recovered = ECDSA.recover(digest, proof[0:65])
  assert recovered == signer ✓

Step 2 — State Write (CEI):
  attestedBeliefs[beliefHash][msg.sender] = true

Step 3 — ZK Proof Verification:
  publicInputs = [beliefHash, msg.sender, 0, 0, 8000, block.timestamp]
  zkProof = proof[65:]
  ok = verifier.verifyProof(zkProof, publicInputs)
  assert ok == true ✓

Step 4 — Event:
  emit BeliefAttested(msg.sender, beliefHash, block.timestamp)
```

### Dependencies + Rebuild Recipe

```
vf-audit-contracts/
├── IStarkVerifier.sol (local copy)
└── DilithiumAttestorV3.sol
@openzeppelin/contracts/utils/cryptography/ECDSA.sol
```

```bash
forge build --contracts vf-audit-contracts/DilithiumAttestorV3.sol
```

---

## 10. v3/DilithiumAttestorV3.sol

**Path:** `/home/user/workspace/v3/DilithiumAttestorV3.sol`
**Role:** Belief verifier — V3 staging copy; import path differs, 4-element publicInputs

### Summary

This is a staging copy of `DilithiumAttestorV3` in the `/v3/` directory. It applies the HIGH-1 per-user mapping fix and the HIGH-2 `msg.sender` ECDSA binding fix, but its `publicInputs` array has only **4 elements** (not 6) — meaning `beliefScore` and `block.timestamp` are absent. This is the intermediate state before the CRIT-07 fix was applied. It imports `IStarkVerifier` from `./IStarkVerifier.sol` (relative path within the v3 directory). The import for `RewardStream` uses `../deprecated/RewardStream.sol`.

### Full Source

*(Identical to §9 except `publicInputs` is 4 elements with epoch=0, moduleID=0, and state write is after the external call in this version.)*

Key differences vs §9:

```solidity
// Line 52: 4-element array (no beliefScore, no timestamp)
uint256[] memory publicInputs = new uint256[](4);
publicInputs[0] = uint256(beliefHash);
publicInputs[1] = uint256(uint160(msg.sender));
publicInputs[2] = 0; // epoch
publicInputs[3] = 0; // moduleID

// State write AFTER external call (CEI not fully applied)
bytes memory zkProof = proof[65:];
bool ok = verifier.verifyProof(zkProof, publicInputs);
if (!ok) revert ProofVerificationFailed();

attestedBeliefs[beliefHash][msg.sender] = true; // after external call
```

### Line-by-Line Walkthrough

Same structure as §9 except:
- **Line 52–57:** `publicInputs` has 4 elements. Missing `beliefScore` (index 4) and `block.timestamp` (index 5). This means the ZK verifier in this configuration expects `getPublicInputsCount() == 4`.
- **CEI:** state write appears after the external `verifyProof` call. Reentrancy guard is the `AlreadyAttested` check at the top, which is sufficient if the mapping is already set — but since it is not yet set when the external call occurs, a malicious verifier could re-enter.

### State Variables / Functions / Events

Same as §9 except `publicInputs` array length = 4.

### Security Invariants

- CRIT-07 **not fixed** in this version — 4-element publicInputs may not match a verifier configured for 6 inputs.
- CEI **partially violated** — state write after external call.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts v3/DilithiumAttestorV3.sol
```

---

## 11. v3-pr-ready/contracts/v3/DilithiumAttestorV3.sol

**Path:** `/home/user/workspace/v3-pr-ready/contracts/v3/DilithiumAttestorV3.sol`
**Role:** Belief verifier — PR-ready V3 with import path `../contracts/IStarkVerifier.sol`, 4-element publicInputs, no CRIT-07 fix

### Summary

The PR-ready staging version. Structurally equivalent to §10 — per-user mapping and ECDSA `msg.sender` binding are applied, but `publicInputs` remains 4 elements and the CRIT-07 fix (beliefScore + timestamp in public inputs) has not been applied. The import path differs: `import {IStarkVerifier} from "../contracts/IStarkVerifier.sol"` — referencing the root contracts folder rather than a local copy.

### Key Differences vs §9

```solidity
import {IStarkVerifier} from "../contracts/IStarkVerifier.sol"; // different import path

// 4-element publicInputs (CRIT-07 not applied)
uint256[] memory publicInputs = new uint256[](4);
```

All other logic identical to §10.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts v3-pr-ready/contracts/v3/DilithiumAttestorV3.sol
```

---

## 12. push_contracts_v3_DilithiumAttestorV3.sol

**Path:** `/home/user/workspace/push_contracts_v3_DilithiumAttestorV3.sol`
**Role:** Belief verifier — Final pushed V3 with CRIT-07 fix applied (6-element publicInputs)

### Summary

This is the **final pushed version** of `DilithiumAttestorV3`, generated during the automated `push_contracts_v3` workflow. It is identical to the audit version (§9) in all critical respects: per-user mapping (HIGH-1), `msg.sender`-bound ECDSA (HIGH-2), 6-element `publicInputs` including `beliefScore=8000` and `block.timestamp` (CRIT-07), and CEI-compliant state write. This is the version that should be deployed to production.

### Key Feature: CRIT-07 Fix

```solidity
uint256[] memory publicInputs = new uint256[](6);
publicInputs[0] = uint256(beliefHash);
publicInputs[1] = uint256(uint160(msg.sender));
publicInputs[2] = 0; // epoch
publicInputs[3] = 0; // moduleID
publicInputs[4] = 8000; // beliefScore — V3 journal field
publicInputs[5] = block.timestamp; // timestamp — V3 journal field
```

This matches the 6-field V3 journal ABI encoding and the `PUBLIC_INPUTS_COUNT = 6` constant in `ProductionBeliefAttestationVerifierV3` (§16, §19).

All other details identical to §9.

### Dependencies + Rebuild Recipe

Identical to §9. Use the root `forge build`.

---
## 13. contracts/BeliefAttestationVerifier.sol

**Path:** `/home/user/workspace/contracts/BeliefAttestationVerifier.sol`
**Role:** ZK proof verifier — DEV ONLY; chain ID guard (31337/1337); returns true for any proof ≥32 bytes

### Summary

`BeliefAttestationVerifier` is the **development-mode-only** verifier. It is intentionally non-functional on mainnet: its constructor enforces that `block.chainid` is either `31337` (Anvil/Hardhat default) or `1337` (Ganache). Any deployment attempt on a production chain reverts during construction. When operating on a dev chain, `verifyProof` accepts any proof whose length is at least 32 bytes and returns `true`, bypassing all cryptographic checks. It emits a `DevVerifierUsed` event as a loud warning. It implements `IStarkVerifier` so it is a drop-in for local development. `BeliefAttestationVerifierProduction` (§14) is its production replacement.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import {IStarkVerifier} from "./IStarkVerifier.sol";
 5
 6  /// @title BeliefAttestationVerifier
 7  /// @notice DEV-ONLY verifier. Reverts at construction on mainnet (chain ≠ 31337/1337).
 8  /// @dev WARNING: Accepts any proof ≥ 32 bytes. NEVER deploy to mainnet.
 9  contract BeliefAttestationVerifier is IStarkVerifier {
10
11      string public constant PROOF_SYSTEM_ID =
12          "DEV-ONLY-BeliefAttestation-Do-Not-Use-In-Production";
13
14      uint256 public constant PUBLIC_INPUTS_COUNT = 4;
15
16      event DevVerifierUsed(
17          address indexed caller,
18          bytes32 indexed beliefHash,
19          uint256 chainId
20      );
21
22      error NotDevChain(uint256 chainId);
23
24      constructor() {
25          if (block.chainid != 31337 && block.chainid != 1337) {
26              revert NotDevChain(block.chainid);
27          }
28      }
29
30      /// @notice Dev stub: accepts any proof ≥ 32 bytes.
31      function verifyProof(
32          bytes calldata proofBytes,
33          uint256[] calldata publicInputs
34      ) external override returns (bool) {
35          // Minimum sanity check — reject obviously empty proofs
36          require(proofBytes.length >= 32, "proof too short");
37          require(publicInputs.length == PUBLIC_INPUTS_COUNT, "bad input count");
38
39          // Extract beliefHash from publicInputs[0] for event emission
40          bytes32 beliefHash = bytes32(publicInputs[0]);
41
42          emit DevVerifierUsed(msg.sender, beliefHash, block.chainid);
43          return true;
44      }
45
46      function getPublicInputsCount()
47          external pure override returns (uint256)
48      {
49          return PUBLIC_INPUTS_COUNT;
50      }
51
52      function getProofSystemId()
53          external pure override returns (string memory)
54      {
55          return PROOF_SYSTEM_ID;
56      }
57  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 11–12 | `PROOF_SYSTEM_ID` contains the string `"DEV-ONLY"` — an explicit identifier that prevents confusion with production verifiers in off-chain tooling. |
| 14 | `PUBLIC_INPUTS_COUNT = 4` — matches V1/V2 verifier format. |
| 16–20 | `DevVerifierUsed` event — emitted on every `verifyProof` call, providing a clear on-chain audit trail if this verifier is accidentally used on a testnet with chain ID ≠ 31337. |
| 24–28 | Constructor chain guard: `block.chainid != 31337 && block.chainid != 1337` — deployment reverts on any other chain. This is a deployment-time safety net; it does not protect against state forks. |
| 36 | `proofBytes.length >= 32` — minimal sanity check to reject empty/very short proofs. No cryptographic verification. |
| 37 | `publicInputs.length == PUBLIC_INPUTS_COUNT` — enforces correct array length even in dev mode. |
| 43 | Returns `true` unconditionally for any passing input. |

### State Variables

None (stateless verifier).

### Functions

**`constructor()`** — Reverts if `block.chainid` is not 31337 or 1337.

**`verifyProof(bytes, uint256[]) external returns (bool)`** — Accepts any proof ≥32 bytes. Emits `DevVerifierUsed`. Returns `true`.

**`getPublicInputsCount() external pure returns (uint256)`** — Returns 4.

**`getProofSystemId() external pure returns (string memory)`** — Returns `"DEV-ONLY-BeliefAttestation-Do-Not-Use-In-Production"`.

### Events

| Event | When | Fields |
|-------|------|--------|
| `DevVerifierUsed(address caller, bytes32 beliefHash, uint256 chainId)` | Every `verifyProof` call | All fields logged; easy to monitor |

### Security Invariants

1. Construction-time chain ID guard is the primary protection. It cannot be bypassed without deploying on the correct chain ID.
2. **Critical:** If a state fork or replay makes `block.chainid == 31337` on a non-dev network, this contract would accept all proofs. This is a theoretical risk only.
3. The `DevVerifierUsed` event provides a loud on-chain signal if the contract is accidentally wired into a non-dev deployment.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/BeliefAttestationVerifier.sol
```

---

## 14. contracts/BeliefAttestationVerifierProduction.sol

**Path:** `/home/user/workspace/contracts/BeliefAttestationVerifierProduction.sol`
**Role:** ZK proof verifier — Production V2; immutable verifier+imageId; sha256 journalDigest (CRITICAL-004 fix); returns false on catch (not revert)

### Summary

`BeliefAttestationVerifierProduction` is the V2 production replacement for the dev verifier. It stores an immutable `IRiscZeroVerifier riscZeroVerifier` and an immutable `bytes32 imageId`, wraps proof verification in a `try/catch` returning `false` on failure (rather than propagating reverts), and emits a `ProofVerificationFailed` event on catch. The CRITICAL-004 fix is the use of `sha256` for journal digests (as required by RISC Zero) rather than `keccak256`. It stores a `hasAttestation[bytes32][address] => bool` mapping (not epoch-aware).

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import {IRiscZeroVerifier} from "./IRiscZeroVerifier.sol";
 5  import {IStarkVerifier} from "./IStarkVerifier.sol";
 6
 7  /// @title BeliefAttestationVerifierProduction
 8  /// @notice V2 production verifier. Immutable verifier+imageId. CRITICAL-004: sha256 journal digest.
 9  contract BeliefAttestationVerifierProduction is IStarkVerifier {
10
11      string public constant PROOF_SYSTEM_ID =
12          "RISC0-STARK-BeliefAttestation-Production-v2.0";
13
14      uint256 public constant PUBLIC_INPUTS_COUNT = 4;
15      uint256 public constant MIN_BELIEF_THRESHOLD = 8000;
16
17      IRiscZeroVerifier public immutable riscZeroVerifier;
18      bytes32 public immutable imageId;
19
20      mapping(bytes32 => mapping(address => bool)) public hasAttestation;
21      uint256 public attestationCount;
22
23      struct VerifiedAttestation {
24          bytes32 beliefHash;
25          address attester;
26          uint256 epoch;
27          uint256 moduleId;
28          uint256 beliefScore;
29          uint256 verifiedAt;
30          bool exists;
31      }
32
33      mapping(bytes32 => VerifiedAttestation) public attestations;
34
35      event AttestationVerified(
36          bytes32 indexed beliefHash,
37          address indexed attester,
38          uint256 epoch,
35          uint256 moduleId,
36          uint256 beliefScore,
37          uint256 timestamp
38      );
39
40      event ProofVerificationFailed(
41          bytes32 indexed beliefHash,
42          address indexed attester,
43          string reason
44      );
45
46      error ZeroAddress();
47      error ZeroImageId();
48      error InvalidPublicInputsCount();
49      error InvalidBeliefHash();
50      error InvalidAttesterAddress();
51      error InvalidEpoch();
52      error EmptyProof();
53
54      constructor(address _riscZeroVerifier, bytes32 _imageId) {
55          if (_riscZeroVerifier == address(0)) revert ZeroAddress();
56          if (_imageId == bytes32(0)) revert ZeroImageId();
57          riscZeroVerifier = IRiscZeroVerifier(_riscZeroVerifier);
58          imageId = _imageId;
59      }
60
61      function verifyProof(
62          bytes calldata proofBytes,
63          uint256[] calldata publicInputs
64      ) external override returns (bool) {
65          if (publicInputs.length != PUBLIC_INPUTS_COUNT)
66              revert InvalidPublicInputsCount();
67
68          bytes32 beliefHash = bytes32(publicInputs[0]);
69          address attester   = address(uint160(publicInputs[1]));
70          uint256 epoch      = publicInputs[2];
71          uint256 moduleId   = publicInputs[3];
72
73          if (beliefHash == bytes32(0)) revert InvalidBeliefHash();
74          if (attester == address(0))   revert InvalidAttesterAddress();
75          if (epoch > type(uint32).max) revert InvalidEpoch();
76          if (proofBytes.length == 0)   revert EmptyProof();
77
78          // CRITICAL-004 FIX: sha256 journal digest (not keccak256)
79          bytes memory journal = abi.encode(beliefHash, attester, epoch, moduleId);
80          bytes32 journalDigest = sha256(journal);
81
82          try riscZeroVerifier.verify(proofBytes, imageId, journalDigest) returns (bool ok) {
83              if (!ok) {
84                  emit ProofVerificationFailed(beliefHash, attester, "verifier returned false");
85                  return false;
86              }
87          } catch Error(string memory reason) {
88              emit ProofVerificationFailed(beliefHash, attester, reason);
89              return false;
90          } catch {
91              emit ProofVerificationFailed(beliefHash, attester, "unknown revert");
92              return false;
93          }
94
95          bytes32 key = keccak256(abi.encodePacked(beliefHash, attester, epoch));
96          attestations[key] = VerifiedAttestation({
97              beliefHash:  beliefHash,
98              attester:    attester,
99              epoch:       epoch,
100             moduleId:    moduleId,
101             beliefScore: MIN_BELIEF_THRESHOLD,
102             verifiedAt:  block.timestamp,
103             exists:      true
104         });
105
106         hasAttestation[beliefHash][attester] = true;
107         attestationCount++;
108
109         emit AttestationVerified(
110             beliefHash, attester, epoch, moduleId,
111             MIN_BELIEF_THRESHOLD, block.timestamp
112         );
113         return true;
114     }
115
116     function getPublicInputsCount() external pure override returns (uint256) {
117         return PUBLIC_INPUTS_COUNT;
118     }
119
120     function getProofSystemId() external pure override returns (string memory) {
121         return PROOF_SYSTEM_ID;
122     }
123
124     function getAttestation(bytes32 beliefHash, address attester, uint256 epoch)
125         external view returns (VerifiedAttestation memory)
126     {
127         bytes32 key = keccak256(abi.encodePacked(beliefHash, attester, epoch));
128         return attestations[key];
129     }
130
131     function isAttested(bytes32 beliefHash, address attester)
132         external view returns (bool)
133     {
134         return hasAttestation[beliefHash][attester];
135     }
136  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 17–18 | Both `riscZeroVerifier` and `imageId` are **immutable** — no owner, no upgrade path. Maximally trustless but inflexible. |
| 20 | `hasAttestation[beliefHash][attester]` — 2-level mapping, **not epoch-aware**. Any attestation for a `(beliefHash, attester)` pair marks the pair as attested across all epochs. |
| 78–80 | **CRITICAL-004 FIX:** `sha256(abi.encode(...))` rather than `keccak256`. SHA-256 is required because the RISC Zero guest program computes its journal digest using SHA-256. |
| 82 | `try ... returns (bool ok)` — uses the `IRiscZeroVerifier` variant that returns `bool`. The `catch` paths log events but return `false` rather than bubbling the revert. This means callers may silently receive `false` and not handle it appropriately. |
| 95 | Attestation key: `keccak256(abi.encodePacked(beliefHash, attester, epoch))`. |
| 101 | `beliefScore` recorded as `MIN_BELIEF_THRESHOLD` (8000) because the 4-element publicInputs format does not include the actual belief score from the journal. |

### Math

Journal digest (CRITICAL-004):
\[\text{journalDigest} = \text{SHA-256}(\text{abi.encode}(\text{beliefHash},\, \text{attester},\, \text{epoch},\, \text{moduleId}))\]

### Events

| Event | When | Fields |
|-------|------|--------|
| `AttestationVerified(beliefHash, attester, epoch, moduleId, beliefScore, timestamp)` | Successful verification | All indexed where applicable |
| `ProofVerificationFailed(beliefHash, attester, reason)` | Verification failure (catch branch) | Reason string included |

### Security Invariants

1. `riscZeroVerifier` and `imageId` are immutable — no upgrade path (intentional for trustlessness).
2. `hasAttestation` is not epoch-aware — attestations from any epoch mark the pair permanently.
3. Silent `false` return on verification failure may cause callers to silently accept invalid attestations if they don't check the return value.
4. No `MIN_BELIEF_THRESHOLD` enforcement at the smart contract level for the actual belief score from the proof (score is stored as the minimum threshold value, not the actual proved value).

### ZK Proof Flow

```
verifyProof(proofBytes, [beliefHash, attester, epoch, moduleId])
  │
  ├─ Validate inputs (length, non-zero checks)
  │
  ├─ journal = abi.encode(beliefHash, attester, epoch, moduleId)
  │  journalDigest = sha256(journal)  ← CRITICAL-004 fix
  │
  ├─ try riscZeroVerifier.verify(proofBytes, imageId, journalDigest)
  │   ├─ success → record attestation, emit AttestationVerified, return true
  │   ├─ catch Error(reason) → emit ProofVerificationFailed, return false
  │   └─ catch → emit ProofVerificationFailed("unknown revert"), return false
  │
  └─ attestationCount++
```

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/BeliefAttestationVerifierProduction.sol
```

---

## 15. contracts/ProductionBeliefAttestationVerifier.sol

**Path:** `/home/user/workspace/contracts/ProductionBeliefAttestationVerifier.sol`
**Role:** ZK proof verifier — V2 production verifier; PUBLIC_INPUTS_COUNT=4; hasAttestation not epoch-aware; verifyAttestation returns bool on catch; PROOF_SYSTEM_ID="…v2.0"

### Summary

`ProductionBeliefAttestationVerifier` is the V2 production verifier in the root `contracts/` directory. It wraps both the legacy `verifyProof(bytes, uint256[])` interface and a direct `verifyAttestation(seal, journalData)` interface. The attestation record mapping has two levels: `hasAttestation[beliefHash][attester] => bool` (not epoch-aware). On proof failure in `verifyAttestation`, it catches the revert and returns `false` without emitting a detailed reason. This is the version targeted for replacement by V3.

### Full Source

*(Summarized — key differences from §14 and §20 documented below.)*

**Key characteristics:**
- `PUBLIC_INPUTS_COUNT = 4` (legacy, no beliefScore or timestamp).
- `hasAttestation[bytes32][address] => bool` — 2-level, not epoch-aware.
- `verifyAttestation` returns `false` on catch (like §14).
- `PROOF_SYSTEM_ID = "RISC0-STARK-BeliefAttestation-Production-v2.0"`.
- No timelock on `imageId` updates (owner can update immediately).
- `attestationCount` tracks total verified attestations.

See §20 for the risc0 sub-package copy which adds a 48-hour timelock.

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `riscZeroVerifier` | `IRiscZeroVerifier` | immutable | RISC Zero verifier router |
| `imageId` | `bytes32` | mutable (owner) | Guest ELF hash |
| `owner` | `address` | mutable | Admin for imageId updates |
| `attestations` | `mapping(bytes32 => VerifiedAttestation)` | mutable | Attestation record store |
| `hasAttestation` | `mapping(bytes32 => mapping(address => bool))` | mutable | Cross-epoch attestation flag |
| `attestationCount` | `uint256` | mutable | Total verified count |

### Functions

**`verifyProof(bytes, uint256[]) external returns (bool)`** — Legacy 4-input interface. Reconstructs 4-field journal, calls `riscZeroVerifier.verify`, returns bool.

**`verifyAttestation(bytes seal, bytes journalData) external returns (bool)`** — Direct interface. Decodes full 6-field journal `(beliefHash, attester, epoch, moduleId, beliefScore, timestamp)` from `journalData`, calls verifier, records on success.

### Security Invariants

1. `imageId` mutable with no timelock — owner can silently change the expected guest program.
2. `hasAttestation` not epoch-aware — old proofs remain valid after epoch rotation.

---

## 16. vf-audit-contracts/ProductionBeliefAttestationVerifierV3.sol

**Path:** `/home/user/workspace/vf-audit-contracts/ProductionBeliefAttestationVerifierV3.sol`
**Role:** ZK proof verifier — V3 audit; PUBLIC_INPUTS_COUNT=6; 3-level epoch-aware hasAttestation; reverts on proof failure; timelocked imageId; MIN_BELIEF_THRESHOLD enforcement

### Summary

This is the full audit-grade V3 production verifier. It resolves every high and critical finding from the V2 audit: (1) `PUBLIC_INPUTS_COUNT = 6` to match the V3 journal format; (2) `hasAttestation[beliefHash][attester][epoch] => bool` — three-level mapping that is epoch-aware; (3) `verifyAttestation` **reverts** on proof failure rather than returning `false`; (4) `imageId` updates require a 48-hour timelock; (5) `_recordAttestation` enforces `MIN_BELIEF_THRESHOLD = 8000` and rejects attestations where `beliefScore < 8000`; (6) `attestationCount` is unique per attestation key (no double-counting). This contract is paired with `DilithiumAttestorV3` (§9) and is deployed to Base Mainnet.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import {IRiscZeroVerifier} from "./IRiscZeroVerifier.sol";
 5  import {IStarkVerifier} from "./IStarkVerifier.sol";
 6
 7  /// @title ProductionBeliefAttestationVerifierV3
 8  /// @notice V3 audit: 6-element publicInputs, epoch-aware mapping, revert on failure,
 9  ///         48-hour imageId timelock, MIN_BELIEF_THRESHOLD enforcement.
10  contract ProductionBeliefAttestationVerifierV3 is IStarkVerifier {
11
12      string public constant PROOF_SYSTEM_ID =
13          "RISC0-STARK-BeliefAttestation-Production-v3.0";
14
15      uint256 public constant PUBLIC_INPUTS_COUNT = 6;
16      uint256 public constant MIN_BELIEF_THRESHOLD = 8000;
17      uint256 public constant MAX_EPOCH = type(uint32).max;
18      uint256 public constant IMAGE_ID_TIMELOCK_DELAY = 48 hours;
19
20      IRiscZeroVerifier public immutable riscZeroVerifier;
21
22      address public owner;
23      bytes32 public imageId;
24      uint256 public attestationCount;
25
26      // V3 FIX: 3-level epoch-aware mapping
27      mapping(bytes32 => mapping(address => mapping(uint256 => bool)))
28          public hasAttestation;
29
30      mapping(bytes32 => VerifiedAttestation) public attestations;
31
32      // Timelock state
33      bytes32 public pendingImageId;
34      uint256 public pendingImageIdEffectiveAt;
35
36      struct VerifiedAttestation {
37          bytes32 beliefHash;
38          address attester;
39          uint256 epoch;
40          uint256 moduleId;
41          uint256 beliefScore;
42          uint256 verifiedAt;
43          bool    exists;
44      }
45
46      event AttestationVerified(
47          bytes32 indexed beliefHash,
48          address indexed attester,
49          uint256 epoch,
50          uint256 moduleId,
51          uint256 beliefScore,
52          uint256 timestamp
53      );
54      event VerificationFailed(bytes32 indexed beliefHash, address indexed attester, string reason);
55      event ImageIdUpdated(bytes32 indexed oldId, bytes32 indexed newId);
56      event ImageIdChangeProposed(bytes32 indexed current, bytes32 indexed proposed, uint256 effectiveAt);
57      event ImageIdChangeCancelled(bytes32 indexed cancelledId);
58      event OwnershipTransferred(address indexed prev, address indexed next);
59
60      error ZeroAddress();
61      error ZeroImageId();
62      error InvalidPublicInputsCount();
63      error InvalidBeliefHash();
64      error InvalidAttesterAddress();
65      error InvalidEpoch();
66      error EmptyProof();
67      error ProofVerificationFailed(string reason);
68      error BelowMinBeliefThreshold(uint256 score, uint256 threshold);
69      error AlreadyAttested();
70      error OnlyOwner();
71      error TimelockNotExpired();
72      error NoPendingImageId();
73      error ImageIdAlreadyPending();
74
75      modifier onlyOwner() {
76          if (msg.sender != owner) revert OnlyOwner();
77          _;
78      }
79
80      constructor(address _riscZeroVerifier, bytes32 _imageId) {
81          if (_riscZeroVerifier == address(0)) revert ZeroAddress();
82          if (_imageId == bytes32(0)) revert ZeroImageId();
83          riscZeroVerifier = IRiscZeroVerifier(_riscZeroVerifier);
84          imageId = _imageId;
85          owner = msg.sender;
86          emit OwnershipTransferred(address(0), msg.sender);
87          emit ImageIdUpdated(bytes32(0), _imageId);
88      }
89
90      function verifyProof(
91          bytes calldata proofBytes,
92          uint256[] calldata publicInputs
93      ) external override returns (bool) {
94          if (publicInputs.length != PUBLIC_INPUTS_COUNT)
95              revert InvalidPublicInputsCount(); // expects 6
96
97          bytes32 beliefHash = bytes32(publicInputs[0]);
98          address attester   = address(uint160(publicInputs[1]));
99          uint256 epoch      = publicInputs[2];
100         uint256 moduleId   = publicInputs[3];
101         uint256 beliefScore= publicInputs[4];
102         uint256 timestamp  = publicInputs[5];
103
104         if (beliefHash == bytes32(0)) revert InvalidBeliefHash();
105         if (attester == address(0))   revert InvalidAttesterAddress();
106         if (epoch > MAX_EPOCH)        revert InvalidEpoch();
107         if (proofBytes.length == 0)   revert EmptyProof();
108
109         // Reconstruct journal from 6 fields
110         bytes memory journal = abi.encode(
111             beliefHash, attester, uint32(epoch), uint32(moduleId),
112             uint32(beliefScore), uint64(timestamp)
113         );
114         bytes32 journalDigest = sha256(journal);
115
116         try riscZeroVerifier.verify(proofBytes, imageId, journalDigest) {
117             // Reverts if proof invalid
118         } catch Error(string memory reason) {
119             revert ProofVerificationFailed(reason);
120         } catch {
121             revert ProofVerificationFailed("Verifier reverted");
122         }
123
124         _recordAttestation(beliefHash, attester, epoch, moduleId, beliefScore, timestamp);
125         return true;
126      }
127
128      function verifyAttestation(
129          bytes calldata seal,
130          bytes calldata journalData
131      ) external returns (bool) {
132          if (seal.length == 0) revert EmptyProof();
133
134          bytes32 journalDigest = sha256(journalData);
135
136          try riscZeroVerifier.verify(seal, imageId, journalDigest) {
137              // proceed
138          } catch Error(string memory reason) {
139              revert ProofVerificationFailed(reason);
140          } catch {
141              revert ProofVerificationFailed("Verifier reverted");
142          }
143
144          (
145              bytes32 beliefHash,
146              address attester,
147              uint32 epoch,
148              uint32 moduleId,
149              uint32 beliefScore,
150              uint64 timestamp
151          ) = abi.decode(journalData, (bytes32, address, uint32, uint32, uint32, uint64));
152
153          _recordAttestation(beliefHash, attester, epoch, moduleId, beliefScore, timestamp);
154          return true;
155      }
156
157      function _recordAttestation(
158          bytes32 beliefHash,
159          address attester,
160          uint256 epoch,
161          uint256 moduleId,
162          uint256 beliefScore,
163          uint256 timestamp
164      ) internal {
165          // MIN_BELIEF_THRESHOLD enforcement
166          if (beliefScore < MIN_BELIEF_THRESHOLD)
167              revert BelowMinBeliefThreshold(beliefScore, MIN_BELIEF_THRESHOLD);
168
169          // AlreadyAttested guard (per epoch)
170          if (hasAttestation[beliefHash][attester][epoch]) revert AlreadyAttested();
171
172          bytes32 key = keccak256(abi.encodePacked(beliefHash, attester, epoch));
173
174          attestations[key] = VerifiedAttestation({
175              beliefHash:  beliefHash,
176              attester:    attester,
177              epoch:       epoch,
178              moduleId:    moduleId,
179              beliefScore: beliefScore,
180              verifiedAt:  timestamp,
181              exists:      true
182          });
183
184          hasAttestation[beliefHash][attester][epoch] = true;
185          attestationCount++;
186
187          emit AttestationVerified(beliefHash, attester, epoch, moduleId, beliefScore, timestamp);
188      }
189
190      // Timelock functions
191      function proposeImageIdChange(bytes32 _newImageId) external onlyOwner {
192          if (_newImageId == bytes32(0)) revert ZeroImageId();
193          if (pendingImageId != bytes32(0)) revert ImageIdAlreadyPending();
194          pendingImageId = _newImageId;
195          pendingImageIdEffectiveAt = block.timestamp + IMAGE_ID_TIMELOCK_DELAY;
196          emit ImageIdChangeProposed(imageId, _newImageId, pendingImageIdEffectiveAt);
197      }
198
199      function executeImageIdChange() external onlyOwner {
200          if (pendingImageId == bytes32(0)) revert NoPendingImageId();
201          if (block.timestamp < pendingImageIdEffectiveAt) revert TimelockNotExpired();
202          bytes32 old = imageId;
203          imageId = pendingImageId;
204          pendingImageId = bytes32(0);
205          pendingImageIdEffectiveAt = 0;
206          emit ImageIdUpdated(old, imageId);
207      }
208
209      function cancelImageIdChange() external onlyOwner {
210          if (pendingImageId == bytes32(0)) revert NoPendingImageId();
211          bytes32 cancelled = pendingImageId;
212          pendingImageId = bytes32(0);
213          pendingImageIdEffectiveAt = 0;
214          emit ImageIdChangeCancelled(cancelled);
215      }
216
217      function transferOwnership(address newOwner) external onlyOwner {
218          if (newOwner == address(0)) revert ZeroAddress();
219          address old = owner;
220          owner = newOwner;
221          emit OwnershipTransferred(old, newOwner);
222      }
223
224      function getPublicInputsCount() external pure override returns (uint256) {
225          return PUBLIC_INPUTS_COUNT;
226      }
227
228      function getProofSystemId() external pure override returns (string memory) {
229          return PROOF_SYSTEM_ID;
230      }
231  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 15 | `PUBLIC_INPUTS_COUNT = 6` — matches V3 journal format with beliefScore and timestamp. |
| 27–28 | **3-level `hasAttestation`**: `beliefHash → attester → epoch → bool`. Epoch-aware: different epochs produce independent attestation records. |
| 67 | `error ProofVerificationFailed(string reason)` — reverts on failure instead of returning `false`. Callers can only succeed by catching the revert. This is stronger than V2's silent `false`. |
| 68 | `error BelowMinBeliefThreshold(uint256 score, uint256 threshold)` — enforces that the actually proved belief score ≥ 8000 bps. V2 stored a hardcoded 8000; V3 validates the actual proved value. |
| 94–95 | `PUBLIC_INPUTS_COUNT` = 6 enforced on `verifyProof`. A V1/V2 caller with 4-element arrays will revert at this check. |
| 110–113 | Journal reconstruction includes all 6 fields using correct types: `uint32` for epoch/moduleId/beliefScore, `uint64` for timestamp. This must match the RISC Zero guest's `abi.encode` call exactly. |
| 116–122 | `try riscZeroVerifier.verify(...)` — uses the Apache `IRiscZeroVerifier` (reverts internally); the `catch` branches re-revert as `ProofVerificationFailed`. |
| 166–167 | `BelowMinBeliefThreshold` guard — actual `beliefScore` from the proof journal is checked. A proof attesting a low-confidence belief is rejected. |
| 170 | `AlreadyAttested` guard scoped to `(beliefHash, attester, epoch)` — can attest again in a different epoch. |
| 191–215 | Timelock functions: `proposeImageIdChange`, `executeImageIdChange`, `cancelImageIdChange`. The 48-hour delay is `IMAGE_ID_TIMELOCK_DELAY`. |

### Math

Journal encoding (V3 6-field standard):
\[\text{journal} = \text{abi.encode}(\underbrace{\text{beliefHash}}_{\text{bytes32}},\, \underbrace{\text{attester}}_{\text{address}},\, \underbrace{\text{epoch}}_{\text{uint32}},\, \underbrace{\text{moduleId}}_{\text{uint32}},\, \underbrace{\text{beliefScore}}_{\text{uint32}},\, \underbrace{\text{timestamp}}_{\text{uint64}})\]

\[\text{journalDigest} = \text{SHA-256}(\text{journal})\]

Timelock:
\[\text{effectiveAt} = \text{block.timestamp} + 172800\;\text{seconds}\]

### Events

| Event | When | Fields |
|-------|------|--------|
| `AttestationVerified` | Successful `_recordAttestation` | beliefHash, attester, epoch, moduleId, beliefScore, timestamp |
| `VerificationFailed` | (reserved, not emitted in this version — revert used instead) | — |
| `ImageIdUpdated` | After `executeImageIdChange` | old, new |
| `ImageIdChangeProposed` | After `proposeImageIdChange` | current, proposed, effectiveAt |
| `ImageIdChangeCancelled` | After `cancelImageIdChange` | cancelled |
| `OwnershipTransferred` | After `transferOwnership` | prev, next |

### Security Invariants

1. `revert` on proof failure — no silent acceptance of invalid proofs.
2. `beliefScore < MIN_BELIEF_THRESHOLD` rejected at contract level.
3. Epoch-aware `hasAttestation` — correct replay protection per epoch.
4. 48-hour timelock on `imageId` changes — stakeholders have time to react.
5. Single `pendingImageId` at a time — prevents proposal spam.

### ZK Proof Flow

```
verifyAttestation(seal, journalData)
  │
  ├─ journalDigest = sha256(journalData)
  │
  ├─ try riscZeroVerifier.verify(seal, imageId, journalDigest)
  │   ├─ success → continue
  │   └─ catch → revert ProofVerificationFailed(reason)
  │
  ├─ abi.decode(journalData) → (beliefHash, attester, epoch, moduleId, beliefScore, timestamp)
  │
  └─ _recordAttestation(...)
      ├─ beliefScore < 8000 → revert BelowMinBeliefThreshold
      ├─ hasAttestation[h][a][e] → revert AlreadyAttested
      ├─ write attestations[key]
      ├─ hasAttestation[h][a][e] = true
      ├─ attestationCount++
      └─ emit AttestationVerified
```

### Dependencies + Rebuild Recipe

```bash
forge build --contracts vf-audit-contracts/ProductionBeliefAttestationVerifierV3.sol
```

---

## 17. v3/ProductionBeliefAttestationVerifierV3.sol

**Path:** `/home/user/workspace/v3/ProductionBeliefAttestationVerifierV3.sol`
**Role:** ZK proof verifier — V3 staging; PUBLIC_INPUTS_COUNT=4 (different from audit!)

### Summary

Staging copy of the V3 verifier in `/v3/`. Structurally similar to §16 but with `PUBLIC_INPUTS_COUNT = 4` — a significant regression from the audit version. This means the verifier in this directory expects only 4 public inputs, which would be incompatible with `DilithiumAttestorV3` (§9) which constructs 6 elements. This is likely a merge conflict artifact where the V3 audit changes were partially applied. All timelock machinery is present.

### Key Difference vs §16

```solidity
uint256 public constant PUBLIC_INPUTS_COUNT = 4; // vs 6 in audit version
```

The journal reconstruction therefore only uses 4 fields:
```solidity
bytes memory journal = abi.encode(beliefHash, attester, uint32(epoch), uint32(moduleId));
// beliefScore and timestamp omitted
```

This creates a **critical mismatch**: if deployed alongside a `DilithiumAttestorV3` that sends 6-element publicInputs, the verifier at this path rejects all proofs with `InvalidPublicInputsCount`.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts v3/ProductionBeliefAttestationVerifierV3.sol
```

---

## 18. v3-pr-ready/contracts/v3/ProductionBeliefAttestationVerifierV3.sol

**Path:** `/home/user/workspace/v3-pr-ready/contracts/v3/ProductionBeliefAttestationVerifierV3.sol`
**Role:** ZK proof verifier — PR-ready staging; PUBLIC_INPUTS_COUNT=4

### Summary

Another PR-ready staging copy. Like §17, this version has `PUBLIC_INPUTS_COUNT = 4`. Same critical mismatch with the 6-element attestor. Import paths point to `../contracts/` for IStarkVerifier and `../../../` for IRiscZeroVerifier, reflecting the monorepo layout. Timelock code present. Should be replaced by §19 for deployment.

### Key Differences vs §16

- `PUBLIC_INPUTS_COUNT = 4`
- Import: `import {IStarkVerifier} from "../contracts/IStarkVerifier.sol"`

---

## 19. push_contracts_v3_ProductionBeliefAttestationVerifierV3.sol

**Path:** `/home/user/workspace/push_contracts_v3_ProductionBeliefAttestationVerifierV3.sol`
**Role:** ZK proof verifier — Final pushed V3; PUBLIC_INPUTS_COUNT=6; all audit fixes applied

### Summary

The **final pushed production version**. Equivalent to the audit version (§16) in all critical aspects: `PUBLIC_INPUTS_COUNT = 6`, epoch-aware 3-level `hasAttestation`, revert on proof failure, 48-hour timelock, `MIN_BELIEF_THRESHOLD` enforcement. This is the version that should be deployed to Base Mainnet alongside `push_contracts_v3_DilithiumAttestorV3.sol` (§12).

All details are identical to §16. See §16 for full documentation.

---

## 20. theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol

**Path:** `/home/user/workspace/theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol`
**Role:** ZK proof verifier — risc0 sub-package production verifier; PUBLIC_INPUTS_COUNT=4; implements IStarkVerifier; dual interface (legacy + direct); 48-hour timelock; returns false on catch

### Summary

This is the production verifier deployed from the risc0 sub-package. It implements the `IStarkVerifier` interface for backward compatibility with `DilithiumAttestor` (V1), while also providing a direct `verifyAttestation(seal, journalData)` method. Unlike §16 it has `PUBLIC_INPUTS_COUNT = 4` (legacy format). However, `verifyAttestation` uses the full 6-field journal format (decoding `beliefHash, attester, epoch, moduleId, beliefScore, timestamp`). Key distinction from §16: on proof failure in `verifyAttestation`, it returns `false` (like V2) rather than reverting. It features a **48-hour timelock** on `imageId` updates, a full ownership transfer function, and the complete complement of timelock management functions (`proposeImageIdChange`, `executeImageIdChange`, `cancelImageIdChange`).

### Line-by-Line Walkthrough (Key Lines)

| Line | Description |
|------|-------------|
| 47 | `contract ProductionBeliefAttestationVerifier is IStarkVerifier` — implements legacy interface. |
| 131–132 | `PROOF_SYSTEM_ID = "RISC0-STARK-BeliefAttestation-Production-v2.0"` — V2 identifier even though this has V3 features (timelock). |
| 135 | `PUBLIC_INPUTS_COUNT = 4` — legacy 4-field format for `verifyProof`. |
| 145 | `IMAGE_ID_TIMELOCK_DELAY = 48 hours`. |
| 153 | `riscZeroVerifier` — immutable. |
| 165 | `imageId` — mutable (timelock-protected). |
| 176 | `hasAttestation[beliefHash][attester] => bool` — 2-level, not epoch-aware (same as V2). |
| 235–252 | `verifyProof` — 4-element legacy path, reconstructs 4-field journal. |
| 268–305 | `verifyAttestation` — direct 6-field path; `try/catch` returns `false` on failure. |
| 278 | `try riscZeroVerifier.verify(seal, imageId, journalDigest)` — uses Apache IRiscZeroVerifier (reverts). |
| 298–303 | Catch branches return `false`, emit `VerificationFailed`. |
| 357–385 | `_recordAttestation` — stores attestation, sets `hasAttestation`, increments `attestationCount`. |
| 471–479 | `proposeImageIdChange` — timelock entry point. Emits `ImageIdChangeProposed`. |
| 483–495 | `executeImageIdChange` — executes after `pendingImageIdEffectiveAt`. |
| 500–508 | `cancelImageIdChange` — cancels pending proposal. |
| 512–519 | `transferOwnership` — single-step (not two-step, unlike OZ `Ownable2Step`). |

### Math

Legacy path journal (4 fields):
\[\text{journal} = \text{abi.encode}(\text{beliefHash},\, \text{attester},\, \text{uint32(epoch)},\, \text{uint32(moduleId)})\]

Direct path journal (6 fields):
\[\text{journal} = \text{abi.decode}(\text{journalData}) \rightarrow (\text{bytes32}, \text{address}, \text{uint32}, \text{uint32}, \text{uint32}, \text{uint64})\]

\[\text{journalDigest} = \text{SHA-256}(\text{journalData})\]

### Events

Same event set as §16 plus `VerificationFailed` (emitted on catch in `verifyAttestation`).

### Security Invariants

1. `hasAttestation` is not epoch-aware — same as V2.
2. `verifyAttestation` returns `false` on failure — callers must check return value.
3. 48-hour timelock protects `imageId`.
4. Ownership is single-step transfer (no acceptOwner pattern).

### ZK Proof Flow

```
verifyAttestation(seal, journalData)
  │
  ├─ journalDigest = sha256(journalData)
  │
  ├─ try riscZeroVerifier.verify(seal, imageId, journalDigest)
  │   ├─ success → decode journalData, _recordAttestation, return true
  │   ├─ catch Error → emit VerificationFailed, return false
  │   └─ catch    → emit VerificationFailed("Verifier reverted"), return false
  │
  └─ _recordAttestation:
       hasAttestation[beliefHash][attester] = true
       attestationCount++
       emit AttestationVerified
```

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol
```

---

## 21. contracts/BeliefOracle.sol

**Path:** `/home/user/workspace/contracts/BeliefOracle.sol`
**Role:** Belief Oracle — V1 gateway; resonance scoring; global bonusApplied; unbounded batchQuery exposed

### Summary

`BeliefOracle` is the V1 oracle gateway contract. It receives vow strings from seekers, hashes them, delegates belief attestation to `DilithiumAttestor`, and computes a deterministic resonance score in the range [0, 100]. If the resonance exceeds `BONUS_THRESHOLD = 80` and no bonus has been applied for that `vowHash` (globally), it calls `RewardStream.updateMultiplier` with `BONUS_MULTIPLIER = 120`. The V1 vulnerabilities are: (1) `bonusApplied[vowHash]` is global — once any user triggers the bonus for a vow hash, no future user can get a bonus (HIGH-1); (2) `batchQuery` is unbounded (MEDIUM-1); (3) the attestor call happens before state writes (MEDIUM-2 CEI violation).

### Full Source

*(163 lines — full source held in context.)*

Key lines:

```solidity
mapping(bytes32 => bool) public bonusApplied; // V1: global, HIGH-1 vulnerability

function queryBelief(string memory vow, bytes calldata zkSig) public virtual returns (uint256) {
    bytes32 vowHash = keccak256(bytes(vow));
    attestor.attestBelief(vowHash, zkSig); // external call BEFORE state writes — CEI violation

    uint256 resonance = _resolveResonance(vowHash, msg.sender);
    _cachedResonance[vowHash] = resonance;
    lastResonance[msg.sender] = resonance;

    bool applied = false;
    if (!resonanceDrifted && resonance > BONUS_THRESHOLD && !bonusApplied[vowHash]) {
        bonusApplied[vowHash] = true; // global flag
        try rewardStream.updateMultiplier(msg.sender, BONUS_MULTIPLIER) {
            applied = true;
        } catch { applied = false; }
    }
    emit ResonanceQueried(msg.sender, vowHash, resonance, applied);
    return resonance;
}

function batchQuery(...) external { // UNBOUNDED — no length check
    ...
}
```

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `attestor` | `DilithiumAttestor` | immutable | Belief attestation backend |
| `rewardStream` | `RewardStream` | immutable | Reward multiplier updater |
| `guardian` | `address` | mutable | Admin for drift controls and guardian rotation |
| `ghostEcho` | `address` | immutable | Entropy source for resonance formula |
| `resonanceDrifted` | `bool` | mutable | Disables bonus multiplier when `true` |
| `_cachedResonance` | `mapping(bytes32 => uint256)` | private | Per-vowHash resonance cache |
| `lastResonance` | `mapping(address => uint256)` | public | Per-user last resonance score |
| `bonusApplied` | `mapping(bytes32 => bool)` | public | **Global** bonus flag (HIGH-1) |

### Functions

**`queryBelief(string, bytes) public virtual returns (uint256)`**
- Hashes vow → vowHash.
- Calls `attestor.attestBelief` (external, before state writes — CEI violation).
- Resolves resonance via `_resolveResonance`.
- Updates `_cachedResonance` and `lastResonance`.
- If bonus-eligible (global check), sets global `bonusApplied[vowHash] = true` and calls `rewardStream.updateMultiplier`.
- Emits `ResonanceQueried`.

**`batchQuery(string[], bytes[]) external`** — Unbounded loop. HIGH gas cost potential.

**`batchQueryBounded(string[], bytes[]) external`** — Bounded at `MAX_BATCH_QUERY = 50`.

**`previewResonance(string, address) external view`** — Returns `_resolveResonance` without state changes.

**`cachedResonance(bytes32) external view`** — Returns cached resonance for a vowHash.

**`setResonanceDrift(bool) external onlyGuardian`** — Toggles bonus eligibility.

**`updateGuardian(address) external onlyGuardian`** — Single-step guardian rotation (no two-step confirmation).

### Math

Resonance formula:
\[\text{resonance} = \text{uint256}(\text{keccak256}(\text{abi.encodePacked}(\text{vowHash},\, \text{seeker},\, \text{ghostEcho}))) \mod 101\]

Range: [0, 100] inclusive. The modulus is 101 because `MAX_RESONANCE + 1 = 101`.

Bonus multiplier: applied as 120% (not computed on-chain; `BONUS_MULTIPLIER = 120` is passed to `RewardStream.updateMultiplier`).

### Events

| Event | When | Fields |
|-------|------|--------|
| `ResonanceQueried(address seeker, bytes32 vowHash, uint256 resonance, bool multiplierApplied)` | Every `queryBelief` call | seeker, vowHash indexed |
| `ResonanceDriftSet(bool active, address guardian)` | `setResonanceDrift` | active, guardian |
| `GuardianUpdated(address prev, address next)` | `updateGuardian` | prev, next |

### Security Invariants

1. **HIGH-1 (unfixed):** Global `bonusApplied[vowHash]` — one user's bonus blocks all future bonuses for the same vow.
2. **MEDIUM-1 (partial):** `batchQuery` is unbounded; `batchQueryBounded` exists but the unbounded version is still callable.
3. **MEDIUM-2 (unfixed):** External `attestor.attestBelief` called before state writes.
4. Resonance formula is gameable via `previewResonance` (acknowledged LOW finding).

---

## 22. v3/BeliefOracleV3.sol

**Path:** `/home/user/workspace/v3/BeliefOracleV3.sol`
**Role:** Belief Oracle — V3 staging; all V3 audit fixes; identical to v3-pr-ready except import path `../deprecated/RewardStream.sol`

### Summary

Staging copy of `BeliefOracleV3` in the `/v3/` directory. Applies all V3 audit fixes: per-user `bonusApplied` (HIGH-1), `MIN_BELIEF_COUNT` gate (HIGH-2), CEI write before external calls (MEDIUM-2), unbounded `batchQuery` removed (MEDIUM-1). Import paths: `DilithiumAttestorV3` from `./DilithiumAttestorV3.sol`, `RewardStream` from `../deprecated/RewardStream.sol`. Functionally identical to §23 and §24 except for import paths and the additional two-step guardian fix present only in §24 (vf-audit).

See §23 for full documentation.

---

## 23. v3-pr-ready/contracts/v3/BeliefOracleV3.sol

**Path:** `/home/user/workspace/v3-pr-ready/contracts/v3/BeliefOracleV3.sol`
**Role:** Belief Oracle — V3 PR-ready; all V3 fixes; bonusApplied inside try block not yet applied

### Summary

The PR-ready version of `BeliefOracleV3`. All V3 high-severity audit fixes are applied. Import: `DilithiumAttestorV3` from `./DilithiumAttestorV3.sol`, `RewardStream` from `../contracts/deprecated/RewardStream.sol`. Key difference from the vf-audit version (§24): the `bonusApplied` flag is set **before** the `try rewardStream.updateMultiplier(...)` block (meaning if the reward call fails, the flag is still set and the user permanently loses bonus eligibility — this is the HIGH-01 finding fixed only in §24). Also uses single-step `updateGuardian` rather than two-step `proposeGuardian/acceptGuardian`.

### Full Source (Key Differences vs §24)

```solidity
if (bonusEligible) {
    bonusApplied[vowHash][msg.sender] = true; // set before try/catch — HIGH-01 unfixed

    try rewardStream.updateMultiplier(msg.sender, BONUS_MULTIPLIER) {
        applied = true;
    } catch {
        applied = false;
        // bonusApplied already set — user cannot retry
    }
}
```

vs §24 (fixed):
```solidity
try rewardStream.updateMultiplier(msg.sender, BONUS_MULTIPLIER) {
    bonusApplied[vowHash][msg.sender] = true; // set only on success
    applied = true;
} catch {
    applied = false;
    // bonusApplied NOT set — user can retry
}
```

All other details (constants, events, functions) identical to §24. See §24 for full documentation.

---

## 24. vf-audit-contracts/BeliefOracleV3.sol

**Path:** `/home/user/workspace/vf-audit-contracts/BeliefOracleV3.sol`
**Role:** Belief Oracle — Full V3 audit; all fixes including HIGH-01 bonusApplied-in-try-block and HIGH-21 two-step guardian

### Summary

This is the **complete final audit** version of `BeliefOracleV3`. It applies every audit finding including the subtler `HIGH-01` (bonusApplied written inside the try-success block, not before it) and `HIGH-21` (two-step `proposeGuardian/acceptGuardian` replaces single-step `updateGuardian`). The `beliefCount` counter is incremented only after successful `attestor.attestBelief` (H-04 fix). All state writes follow strict CEI ordering. This is the version that should be deployed to production.

### Full Source

*(248 lines — full source read above.)*

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 16 | `attestedBeliefs` — now `DilithiumAttestorV3` (per-user). |
| 46 | `bonusApplied[bytes32][address] => bool` — per-user. HIGH-1 fix. |
| 51 | `beliefCount[address] => uint256` — tracks successful attestation count per user. HIGH-2 fix. |
| 62 | `event GuardianProposed(address current, address pending)` — HIGH-21 event. |
| 67 | `error NotPendingGuardian()` — HIGH-21 error. |
| 77 | `address public pendingGuardian` — HIGH-21 staging address. |
| 111–117 | `proposeGuardian(address newGuardian) external onlyGuardian` — sets `pendingGuardian`, emits `GuardianProposed`. |
| 122–130 | `acceptGuardian() external` — caller must be `pendingGuardian`; clears `pendingGuardian`, updates `guardian`, emits `GuardianUpdated`. |
| 139–141 | `queryBelief`: resolves resonance, writes `_cachedResonance` and `lastResonance` BEFORE any external calls. |
| 154–159 | Bonus eligibility check: `beliefCount[msg.sender] >= MIN_BELIEF_COUNT` (must have ≥1 prior attestation). |
| 161–164 | `bonusApplied` flag deliberately **not** set here (HIGH-01 fix note in comments). |
| 177 | `attestor.attestBelief(vowHash, zkSig)` — external call 1. |
| 180 | `beliefCount[msg.sender]++` — incremented AFTER successful attestation (H-04 fix). |
| 190–197 | `try rewardStream.updateMultiplier(...)`: on success, `bonusApplied[vowHash][msg.sender] = true` (HIGH-01 fix — flag only set on successful reward update). |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `attestor` | `DilithiumAttestorV3` | immutable | Belief attestor |
| `rewardStream` | `RewardStream` | immutable | Reward multiplier |
| `guardian` | `address` | mutable (two-step) | Protocol guardian |
| `pendingGuardian` | `address` | mutable | Pending guardian (HIGH-21 two-step) |
| `ghostEcho` | `address` | immutable | Resonance entropy |
| `resonanceDrifted` | `bool` | mutable | Bonus on/off switch |
| `_cachedResonance` | `mapping(bytes32 => uint256)` | private | Per-vow resonance cache |
| `lastResonance` | `mapping(address => uint256)` | public | Per-user last resonance |
| `bonusApplied` | `mapping(bytes32 => mapping(address => bool))` | public | Per-user bonus flag |
| `beliefCount` | `mapping(address => uint256)` | public | Per-user attestation counter |

### Functions

**`queryBelief(string, bytes) public virtual returns (uint256)`**
- Resolves resonance.
- Writes `_cachedResonance`, `lastResonance` (CEI).
- Checks bonus eligibility (beliefCount gate, resonance threshold, drift flag, per-user bonusApplied).
- Calls `attestor.attestBelief` (external 1).
- Increments `beliefCount[msg.sender]` (after external 1 succeeds).
- `try rewardStream.updateMultiplier(...)`: sets `bonusApplied` only on success (HIGH-01 fix).
- Emits `ResonanceQueried`.

**`batchQueryBounded(string[], bytes[]) external`** — Max 50 vows; calls `queryBelief` per vow.

**`previewResonance(string, address) external view`** — View-only resonance preview.

**`proposeGuardian(address) external onlyGuardian`** — Proposes new guardian.

**`acceptGuardian() external`** — Confirms guardian transfer (must be `pendingGuardian`).

**`setResonanceDrift(bool) external onlyGuardian`** — Toggles bonus eligibility.

### Math

Resonance formula (unchanged from V1):
\[\text{resonance} = \text{uint256}(\text{keccak256}(\text{abi.encodePacked}(\text{vowHash},\, \text{seeker},\, \text{ghostEcho}))) \mod 101\]

Bonus eligibility predicate:
\[\text{bonusEligible} = \neg\text{resonanceDrifted} \land (\text{resonance} > 80) \land \neg\text{bonusApplied}[\text{vowHash}][\text{seeker}] \land (\text{beliefCount}[\text{seeker}] \ge 1)\]

### Events

| Event | When | Fields |
|-------|------|--------|
| `ResonanceQueried(address seeker, bytes32 vowHash, uint256 resonance, bool multiplierApplied)` | Every `queryBelief` | seeker, vowHash indexed |
| `ResonanceDriftSet(bool active, address guardian)` | `setResonanceDrift` | active, guardian |
| `GuardianProposed(address current, address pending)` | `proposeGuardian` | both indexed |
| `GuardianUpdated(address prev, address next)` | `acceptGuardian` | both indexed |

### Security Invariants

1. Per-user `bonusApplied` — no global lockout (HIGH-1 fixed).
2. `MIN_BELIEF_COUNT = 1` gate — new accounts cannot immediately claim bonus (HIGH-2 fixed).
3. CEI ordering — state written before external calls (MEDIUM-2 fixed).
4. `beliefCount` incremented after successful external attestation (H-04 fixed).
5. `bonusApplied` set only on successful reward update (HIGH-01 fixed).
6. Two-step guardian transfer — no instant privilege theft (HIGH-21 fixed).
7. `batchQuery` unbounded version removed (MEDIUM-1 fixed).

### ZK Proof Flow

```
queryBelief(vow, zkSig)
  │
  ├─ vowHash = keccak256(vow)
  ├─ resonance = uint256(keccak256(vowHash ++ seeker ++ ghostEcho)) % 101
  │
  ├─ [STATE WRITE — CEI]
  │   _cachedResonance[vowHash] = resonance
  │   lastResonance[seeker] = resonance
  │
  ├─ bonusEligible = !drifted ∧ resonance>80 ∧ !bonusApplied[h][s] ∧ beliefCount[s]≥1
  │
  ├─ [EXTERNAL 1] attestor.attestBelief(vowHash, zkSig)
  │   → DilithiumAttestorV3 verifies ECDSA + ZK proof
  │
  ├─ beliefCount[seeker]++
  │
  ├─ if bonusEligible:
  │   try rewardStream.updateMultiplier(seeker, 120)
  │     ✓ bonusApplied[vowHash][seeker] = true; applied = true
  │     ✗ applied = false (can retry)
  │
  └─ emit ResonanceQueried(seeker, vowHash, resonance, applied)
```

### Dependencies + Rebuild Recipe

```
vf-audit-contracts/
├── DilithiumAttestorV3.sol
├── BeliefOracleV3.sol
deprecated/
└── RewardStream.sol
```

```bash
forge build --contracts vf-audit-contracts/BeliefOracleV3.sol
```

---

## 25. push_contracts_v3_BeliefOracleV3.sol

**Path:** `/home/user/workspace/push_contracts_v3_BeliefOracleV3.sol`
**Role:** Belief Oracle — Final pushed V3; same as §24 with all audit fixes

### Summary

The final auto-generated pushed version of `BeliefOracleV3`. Contains all audit fixes from §24: per-user `bonusApplied`, `MIN_BELIEF_COUNT = 1`, CEI compliance, `bonusApplied` set inside try-success block (HIGH-01), and bounded `batchQueryBounded`. Import: `DilithiumAttestorV3` from `./DilithiumAttestorV3.sol`. Uses single-step `updateGuardian` (the two-step HIGH-21 fix from §24 may not be merged here).

All logic is identical to §24 except as noted. See §24 for full documentation.

---
## 26. contracts/MultiOracleConsensus.sol

**Path:** `/home/user/workspace/contracts/MultiOracleConsensus.sol`
**Role:** Multi-oracle consensus — V1; recursive quickSort; O(n) duplicate detection; no DISPUTE_WINDOW; slash=50%; min stake=10 ETH

### Summary

`MultiOracleConsensus` is the V1 multi-oracle consensus contract. It maintains a registry of staked oracles, collects per-round oracle submissions, computes a median consensus value via a recursive quickSort algorithm, and slashes oracles whose submissions deviate more than 20% from the consensus. Its key V1 characteristics and vulnerabilities: (1) the duplicate submission check `_hasOracleSubmitted` is O(n) linear scan; (2) there is no `DISPUTE_WINDOW` (submissions cannot be challenged post-finalization); (3) no `withdrawSlashPool` or `rewardPool` — slashed ETH is trapped; (4) minimum stake is 10 ETH; (5) recursive quickSort is susceptible to stack overflow on large inputs; (6) reputation formula is complex multi-factor scoring.

### Full Source

*(Key sections excerpted — full source held in context.)*

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

contract MultiOracleConsensus {
    uint256 public constant MIN_STAKE      = 10 ether;
    uint256 public constant MAX_ORACLES    = 100;
    uint256 public constant SLASH_PERCENT  = 50;
    uint256 public constant DEVIATION_BPS  = 2000; // 20%

    struct Oracle {
        address addr;
        uint256 stake;
        uint256 reputation;
        bool    registered;
        uint256 totalSubmissions;
        uint256 accurateSubmissions;
        uint256 slashCount;
        uint256 registeredAt;
    }

    struct Round {
        uint256 roundId;
        uint256 startTime;
        uint256 deadline;
        bool    finalized;
        uint256 consensusValue;
    }

    struct Submission {
        address oracle;
        uint256 value;
        uint256 timestamp;
    }

    mapping(address => Oracle)     public oracles;
    mapping(uint256 => Round)      public rounds;
    mapping(uint256 => Submission[]) internal _submissions;
    uint256 public nextRoundId = 1;
    address[] public oracleAddresses;

    // ... (functions: registerOracle, submitValue, finalizeRound, _quickSort, _computeMedian, _slashOracle)
}
```

### Line-by-Line Walkthrough

| Section | Description |
|---------|-------------|
| `registerOracle` | Requires `msg.value >= MIN_STAKE` (10 ETH). Adds to `oracleAddresses` array. Reputation initialized to 5000. |
| `submitValue(roundId, value)` | Calls `_hasOracleSubmitted(roundId, msg.sender)` — O(n) scan of `_submissions[roundId]`. |
| `finalizeRound(roundId)` | After deadline: calls `_quickSort` (recursive), computes median, slashes oracles outside 20% band. |
| `_quickSort(arr, lo, hi)` | Recursive quickSort. No depth limit → stack overflow risk for large submissions arrays. |
| `_computeMedian(arr)` | After sort: if even length, returns `(arr[n/2-1] + arr[n/2]) / 2` (note: potential overflow for large values, unlike V3's safer formula). |
| `_slashOracle(oracle)` | Reduces `oracles[oracle].stake` by `SLASH_PERCENT = 50%`. Slashed ETH stays in contract (no withdrawSlashPool). |
| `_computeReputation(oracle)` | Reputation = accuracy×7000 + stake×2000 + longevity×1000 − slashCount×2000 |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `oracles` | `mapping(address => Oracle)` | mutable | Oracle registry |
| `rounds` | `mapping(uint256 => Round)` | mutable | Round data |
| `_submissions` | `mapping(uint256 => Submission[])` | internal | Per-round submissions |
| `nextRoundId` | `uint256` | mutable | Round counter |
| `oracleAddresses` | `address[]` | mutable | Oracle list |

### Functions

**`registerOracle() external payable`** — Requires ≥10 ETH stake. Adds oracle.

**`submitValue(uint256 roundId, uint256 value) external`** — O(n) duplicate check. Adds submission.

**`finalizeRound(uint256 roundId) external`** — Recursive sort, median, slash.

**`_hasOracleSubmitted(uint256 roundId, address oracle) internal view returns (bool)`** — O(n) scan. **HIGH-3 vulnerability in V1.**

**`unregisterOracle() external`** — Returns stake minus any slashes.

### Math

Median formula (V1 — potential overflow):
\[\text{median} = \frac{a_{n/2-1} + a_{n/2}}{2}\]
where overflow can occur if both values are near `type(uint256).max`.

Reputation formula:
\[\text{rep} = \left(\frac{\text{accurate}}{\text{total}} \times 10000\right) \times \frac{7000}{10000} + \min(\text{stake}, 50\text{ ETH}) \times \frac{2000}{50\text{ ETH}} + \min(\text{days}, 100) \times \frac{1000}{100} - \text{slashCount} \times 2000\]

Slash:
\[\text{newStake} = \text{stake} \times \frac{100 - \text{SLASH\_PERCENT}}{100} = \text{stake} \times 0.5\]

### Events

| Event | When |
|-------|------|
| `OracleRegistered(address oracle, uint256 stake)` | Registration |
| `ValueSubmitted(uint256 roundId, address oracle, uint256 value)` | Each submission |
| `RoundFinalized(uint256 roundId, uint256 consensusValue)` | Finalization |
| `OracleSlashed(address oracle, uint256 slashAmount)` | Deviation slash |

### Security Invariants

1. **HIGH-3:** O(n) `_hasOracleSubmitted` — gas cost grows with submissions per round.
2. **HIGH-1/HIGH-2:** No `withdrawSlashPool` or `rewardPool` — slashed ETH is permanently locked.
3. **MEDIUM-1:** No `DISPUTE_WINDOW` — finalized consensus cannot be challenged.
4. Recursive quickSort — stack depth = O(log n) average, O(n) worst case.
5. Median formula susceptible to overflow for very large uint256 values.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/MultiOracleConsensus.sol
```

---

## 27. theloopbreaker-site/contracts/MultiOracleConsensus.sol

**Path:** `/home/user/workspace/theloopbreaker-site/contracts/MultiOracleConsensus.sol`
**Role:** Multi-oracle consensus — V1 site copy; identical to §26

### Summary

Byte-for-byte copy of the root `contracts/MultiOracleConsensus.sol` (§26) maintained in the theloopbreaker-site sub-tree for independent compilation. All V1 vulnerabilities apply equally. See §26 for full documentation.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/contracts/MultiOracleConsensus.sol
```

---

## 28. vf-audit-contracts/MultiOracleConsensusV3.sol

**Path:** `/home/user/workspace/vf-audit-contracts/MultiOracleConsensusV3.sol`
**Role:** Multi-oracle consensus — Full V3 audit; Ownable2Step+ReentrancyGuard; commit-reveal; min stake=0.1 ETH; trim 10%/10%; outlier slash=10%; double-slash guard; withdrawStake

### Summary

`MultiOracleConsensusV3` is the full audit V3 consensus contract. It resolves all V1 high, medium, and low findings: (1) commit-reveal scheme — oracles first submit a hash `keccak256(abi.encodePacked(value, salt))` (commit phase), then reveal `(value, salt)` in a separate call, preventing front-running; (2) minimum stake reduced to 0.1 ETH; (3) top and bottom 10% of submissions are trimmed before median computation; (4) outlier slash rate reduced to 10% (from 50%); (5) `AlreadySlashedInRound` double-slash guard; (6) `withdrawStake()` function so unregistered oracles can retrieve their stake; (7) `DISPUTE_WINDOW = 7 days`; (8) O(1) `hasSubmittedRound[roundId][oracle]` mapping replaces O(n) scan; (9) iterative insertion sort replaces recursive quickSort; (10) `RoundVoided` for zero-reveal rounds.

### Full Source

*(Key structures and functions — full source held in context.)*

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract MultiOracleConsensusV3 is Ownable2Step, ReentrancyGuard {
    uint256 public constant MIN_STAKE       = 0.1 ether;
    uint256 public constant MAX_ORACLES     = 100;
    uint256 public constant SLASH_PERCENT   = 10;       // V3: 10% not 50%
    uint256 public constant DEVIATION_BPS   = 2000;     // 20%
    uint256 public constant DISPUTE_WINDOW  = 7 days;   // V3: NEW
    uint256 public constant TRIM_PERCENT    = 10;       // trim top/bottom 10%

    // HIGH-3 FIX: O(1) submission check
    mapping(uint256 => mapping(address => bool)) public hasSubmittedRound;

    // Commit-reveal scheme
    mapping(uint256 => mapping(address => bytes32)) public commitments;
    mapping(uint256 => mapping(address => bool))    public revealed;

    // HIGH-2: Double-slash guard
    mapping(uint256 => mapping(address => bool))    public slashedInRound;

    // HIGH-1: Slash pool and reward pool
    uint256 public slashPool;
    uint256 public rewardPool;

    function withdrawStake() external nonReentrant { ... }
    function commitValue(uint256 roundId, bytes32 commitment) external { ... }
    function revealValue(uint256 roundId, uint256 value, bytes32 salt) external { ... }
    function finalizeRound(uint256 roundId) external { ... }
    function withdrawSlashPool() external onlyOwner { ... }
    function distributeRewards(address[] calldata recipients, uint256[] calldata amounts) external onlyOwner { ... }
}
```

### Line-by-Line Walkthrough

| Feature | Line Range | Description |
|---------|------------|-------------|
| Commit phase | `commitValue` | `commitments[roundId][msg.sender] = keccak256(abi.encodePacked(value, salt))`. Must be called before deadline. |
| Reveal phase | `revealValue` | Verifies `keccak256(value, salt) == commitment`. Stores value. Sets `revealed[roundId][msg.sender] = true`. |
| O(1) duplicate guard | `hasSubmittedRound` | `bool` mapping keyed by `(roundId, oracle)` — O(1) lookup. **HIGH-3 fix.** |
| Trim | `finalizeRound` | After sorting, removes bottom `n * TRIM_PERCENT / 100` and top `n * TRIM_PERCENT / 100` values before computing median. |
| Slash | `_slashOracle` | Reduces stake by 10%. Sets `slashedInRound[roundId][oracle] = true` before slash. |
| Double-slash | `_slashOracle` | `if (slashedInRound[roundId][oracle]) return` — **HIGH-2 fix.** |
| Slash pool | `_slashOracle` | Slash amount goes to `slashPool` (not trapped). |
| `withdrawStake` | `withdrawStake` | Unregistered oracles call this to recover stake. `nonReentrant`. |
| `withdrawSlashPool` | Owner function | Moves `slashPool` ETH to owner. |
| `distributeRewards` | Owner function | Distributes `rewardPool` to specified recipients. |
| `RoundVoided` | `finalizeRound` | Emitted when no oracles revealed (zero-reveal protection). |
| `DISPUTE_WINDOW` | Storage | Disputes must be filed within 7 days of finalization. **MEDIUM-1 fix.** |
| Insertion sort | `_insertionSort` | Iterative (no recursion) — safe against stack overflow. |

### Math

Trim bounds:
\[
\text{trimCount} = \left\lfloor\frac{n \times \text{TRIM\_PERCENT}}{100}\right\rfloor
\]
\[
\text{effective range} = [\text{trimCount},\; n - \text{trimCount})
\]

Median over trimmed set:
\[
\text{median} = \begin{cases}
\text{sorted}[m/2] & m \text{ odd} \\
\frac{\text{sorted}[m/2-1]}{2} + \frac{\text{sorted}[m/2]}{2} + \frac{(\text{sorted}[m/2-1] \bmod 2 + \text{sorted}[m/2] \bmod 2)}{2} & m \text{ even}
\end{cases}
\]
(overflow-safe formula)

Slash:
\[\text{slashAmount} = \text{stake} \times \frac{10}{100}\]

Reputation (unchanged from V1 formula, applied post-slash):
\[\text{rep} = \frac{\text{accurate}}{\text{total}} \times 7000 + \frac{\min(\text{stake}, 50\,\text{ETH})}{50\,\text{ETH}} \times 2000 + \frac{\min(\text{days}, 100)}{100} \times 1000 - \text{slashCount} \times 2000\]

Commit-reveal:
\[\text{commitment} = \text{keccak256}(\text{abi.encodePacked}(\text{value},\, \text{salt}))\]

### Events

| Event | When |
|-------|------|
| `OracleRegistered(address oracle, uint256 stake)` | Registration |
| `ValueCommitted(uint256 roundId, address oracle)` | Commit phase |
| `ValueRevealed(uint256 roundId, address oracle, uint256 value)` | Reveal phase |
| `RoundFinalized(uint256 roundId, uint256 consensusValue)` | Finalization |
| `RoundVoided(uint256 roundId)` | No reveals in round |
| `OracleSlashed(address oracle, uint256 slashAmount, uint256 roundId)` | Slash |
| `StakeWithdrawn(address oracle, uint256 amount)` | `withdrawStake` |

### Security Invariants

1. Commit-reveal prevents value front-running.
2. O(1) `hasSubmittedRound` — no gas bloat (HIGH-3 fixed).
3. Double-slash `slashedInRound` guard (HIGH-2 fixed).
4. `slashPool` is withdrawable by owner (HIGH-1 fixed).
5. `withdrawStake` nonReentrant — safe ETH transfer.
6. Iterative sort — no stack overflow.
7. `DISPUTE_WINDOW` enables post-finalization challenges (MEDIUM-1 fixed).
8. `Ownable2Step` — two-step ownership transfer.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts vf-audit-contracts/MultiOracleConsensusV3.sol
```

---

## 29. v3-pr-ready/contracts/v3/MultiOracleConsensusV3.sol

**Path:** `/home/user/workspace/v3-pr-ready/contracts/v3/MultiOracleConsensusV3.sol`
**Role:** Multi-oracle consensus — V3 PR-ready; all HIGH/MEDIUM/LOW audit fixes

### Summary

The PR-ready version of `MultiOracleConsensusV3`. This is the most complete V3 implementation reviewed in the codebase — it explicitly documents all audit fix annotations (`HIGH-1`, `HIGH-2`, `HIGH-3`, `MEDIUM-1`) in NatSpec comments. Key additions over §28: explicit `distributeRewards(address[], uint256[])` function, complete `withdrawSlashPool()`, and a full `DISPUTE_WINDOW = 7 days` enforcement mechanism. Functionally identical to §28. This is the version that was submitted in the PR.

All documentation from §28 applies. See §28.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts v3-pr-ready/contracts/v3/MultiOracleConsensusV3.sol
```

---

## 30. push_contracts_v3_MultiOracleConsensusV3.sol

**Path:** `/home/user/workspace/push_contracts_v3_MultiOracleConsensusV3.sol`
**Role:** Multi-oracle consensus — Final pushed V3; all fixes identical to §28/§29

### Summary

The final auto-generated pushed version. Identical to §28 in all respects. This is the deployment target. See §28 for full documentation.

---

## 31. contracts/FlourishingMetricsOracle.sol

**Path:** `/home/user/workspace/contracts/FlourishingMetricsOracle.sol`
**Role:** Flourishing metrics oracle — V1; median consensus; MINIMUM_QUORUM=3; MAXIMUM_ORACLES=50; insertion sort; overflow-safe median

### Summary

`FlourishingMetricsOracle` V1 collects metric submissions from registered oracles and computes a median consensus. It uses a round-based model where an owner starts rounds, oracles submit values, and any caller can finalize a round after the deadline. It applies an insertion sort (not recursive quickSort like `MultiOracleConsensus`) which is stack-safe. The median formula is overflow-safe. V1 lacks: per-submission-interval cooldown, weighted sources, EMA TWAP, manipulation detection flag storage.

### Full Source

*(Key sections.)*

```solidity
uint256 public constant MINIMUM_QUORUM  = 3;
uint256 public constant MAXIMUM_ORACLES = 50;
uint256 public constant SUBMISSION_WINDOW = 24 hours;

mapping(address => bool)     public isOracle;
mapping(uint256 => Round)    public rounds;
mapping(uint256 => Submission[]) internal _submissions;
mapping(bytes32 => uint256)  public latestValue;
```

### Line-by-Line Walkthrough

| Function | Description |
|----------|-------------|
| `addOracle(address)` | Owner only; checks `oracleCount < MAXIMUM_ORACLES`. |
| `startRound(bytes32 metricId)` | Owner only; creates round with `SUBMISSION_WINDOW = 24hr` deadline. |
| `submitMetric(uint256 roundId, uint256 value)` | Oracle only; checks round is open and oracle hasn't submitted. |
| `finalizeRound(uint256 roundId)` | Any caller after deadline; requires `MINIMUM_QUORUM = 3` submissions. |
| `_computeMedianFromValues(uint256[])` | Sorts in-place using insertion sort; overflow-safe even-length formula. |

### Math

Overflow-safe even-length median:
\[\text{median} = \frac{a}{2} + \frac{b}{2} + \frac{(a \bmod 2) + (b \bmod 2)}{2}\]

where \(a = \text{sorted}[n/2-1]\) and \(b = \text{sorted}[n/2]\). This avoids overflow that would result from \((a + b) / 2\) when \(a + b > 2^{256} - 1\).

### Events

| Event | When |
|-------|------|
| `OracleAdded(address oracle)` | `addOracle` |
| `RoundStarted(uint256 roundId, bytes32 metricId, uint256 deadline)` | `startRound` |
| `MetricSubmitted(uint256 roundId, address oracle, uint256 value)` | `submitMetric` |
| `ConsensusReached(uint256 roundId, bytes32 metricId, uint256 value, uint256 subCount)` | `finalizeRound` |
| `RoundExpired(uint256 roundId)` | `finalizeRound` when quorum not met |

### Security Invariants

1. Insertion sort — no stack overflow (unlike MultiOracleConsensus V1).
2. Overflow-safe median formula.
3. No deviation filtering — all values included in median (V3 adds ±20% filter).
4. No staleness check on `latestValue` (V3 adds `latestTimestamp`).

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/FlourishingMetricsOracle.sol
```

---

## 32. theloopbreaker-site/contracts/FlourishingMetricsOracle.sol

**Path:** `/home/user/workspace/theloopbreaker-site/contracts/FlourishingMetricsOracle.sol`
**Role:** Flourishing metrics oracle — V1 site copy; identical to §31

### Summary

Byte-for-byte copy of §31. See §31 for full documentation.

---

## 33. push_contracts_v3_FlourishingMetricsOracleV3.sol

**Path:** `/home/user/workspace/push_contracts_v3_FlourishingMetricsOracleV3.sol`
**Role:** Flourishing metrics oracle — Final pushed V3; Ownable2Step; weighted sources; EMA TWAP; manipulation detection; emergencyOverride; MIN_SOURCES=3; MAX_METRIC_AGE=1hr

### Summary

`FlourishingMetricsOracleV3` (pushed version) is the V3 continuous-feed metric oracle using a push model rather than round-based model. Sources submit values continuously; each submission triggers aggregation. It uses an EMA (exponential moving average) TWAP, weighted source aggregation, manipulation detection flagging, and emergency override. This is the contract that implements `IFlourishingMetricsOracle`. Key characteristics: per-source latest value tracking (prevents sybil multi-submission gaming), `DEVIATION_THRESHOLD = 2000 bps` (20%) manipulation flag, and `emergencyOverrideMetric` for owner intervention.

### Full Source

*(Key sections — full 177-line source at test_fixes/contracts/FlourishingMetricsOracleV3.sol was identical to this version's core logic.)*

```solidity
uint256 public constant MIN_SOURCES         = 3;
uint256 public constant MAX_METRIC_AGE      = 1 hours;
uint256 public constant DEVIATION_THRESHOLD = 2000; // 20% in bps

struct AggregatedMetric {
    uint256 twap;
    uint256 latestValue;
    uint64  lastUpdated;
    uint256 sourceCount;
    bool    manipulationFlag;
}

mapping(bytes32 => AggregatedMetric) public metrics;
mapping(bytes32 => mapping(address => uint256)) public latestSourceValue;
mapping(bytes32 => mapping(address => uint256)) public latestSourceTimestamp;
```

### Line-by-Line Walkthrough

| Function | Description |
|----------|-------------|
| `registerSource(address, uint256 weight)` | Owner: sets `SourceInfo{registered:true, weight, submissionCount:0}`. Increments `registeredSourceCount`. |
| `deregisterSource(address)` | Owner: sets `registered = false`. Decrements `registeredSourceCount`. |
| `submitMetric(bytes32 metricId, uint256 value)` | Source: validates registered, non-empty metricId. Appends to `metricHistory`, increments `submissionCount`, stores `latestSourceValue[metricId][msg.sender]` and `latestSourceTimestamp`. Calls `_aggregate`. |
| `_aggregate(bytes32 metricId, uint256 newValue)` | Iterates `sourceList`, sums latest values from registered active sources, checks `< MIN_SOURCES`, computes mean, checks deviation, updates TWAP. |
| `emergencyOverrideMetric(bytes32, uint256)` | Owner: directly writes `latestValue`, `twap`, `lastUpdated`, clears `manipulationFlag`, sets `emergencyOverrideActive[metricId] = true`. |
| `getMetricTWAP(bytes32, uint256)` | Returns `metrics[metricId].twap` (ignores window param). |
| `isMetricFresh(bytes32)` | Returns `block.timestamp - lastUpdated <= MAX_METRIC_AGE`. |
| `getMetric(bytes32)` | Returns full `AggregatedMetric` or reverts if not found or stale. |

### Math

Aggregation (plain mean for push_contracts version, before HIGH-13 weighted fix):
\[\text{mean} = \frac{\sum_{i \in \text{active}} \text{latestSourceValue}[m][s_i]}{|\text{active}|}\]

EMA TWAP:
\[\text{TWAP}_n = \begin{cases} v_0 & \text{if } \text{TWAP}_{n-1} = 0 \\ \frac{3 \times \text{TWAP}_{n-1} + v_n}{4} & \text{otherwise} \end{cases}\]

The coefficient 3:1 gives the previous TWAP 75% weight and the new value 25% weight — a 4-period EMA.

Manipulation detection:
\[\text{deviation} = \frac{|v_n - \text{mean}| \times 10000}{\text{mean}}\]
\[\text{manipulated} = \text{deviation} > \text{DEVIATION\_THRESHOLD} = 2000\]

### Events

| Event | When |
|-------|------|
| `MetricUpdated(bytes32 metricId, uint256 newValue, uint256 twap)` | Successful aggregation |
| `ManipulationFlagged(bytes32 metricId, address source, uint256 deviation)` | Deviation >20% |
| `SourceRegistered(address source, uint256 weight)` | `registerSource` |
| `SourceDeregistered(address source)` | `deregisterSource` |
| `EmergencyOverride(bytes32 metricId, uint256 value)` | `emergencyOverrideMetric` |
| `AggregationDeferred(bytes32 metricId, uint256 activeCount, uint256 required)` | Fewer than MIN_SOURCES active |

### Security Invariants

1. Per-source latest-value tracking prevents one source from submitting N times to dominate the mean (HIGH-FMO-01 fixed).
2. `MIN_SOURCES = 3` prevents single-source manipulation.
3. Manipulation flag does not halt processing — it is an advisory signal only.
4. `emergencyOverrideMetric` is owner-only but unrestricted in value — high privilege.
5. Deregistered source's historical values remain in `metricHistory` but are excluded from future aggregation.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts push_contracts_v3_FlourishingMetricsOracleV3.sol
```

---

## 34. test_fixes/contracts/FlourishingMetricsOracleV3.sol

**Path:** `/home/user/workspace/test_fixes/contracts/FlourishingMetricsOracleV3.sol`
**Role:** Flourishing metrics oracle — V3 test-fixes branch; no submission cooldown (CRIT-10 not yet applied); plain mean aggregation (HIGH-13 not yet applied)

### Summary

The `test_fixes` branch version. It has all V3 base fixes (per-source tracking, MIN_SOURCES gate, registeredSourceCount, manipulation detection, EMA TWAP, emergency override) but is missing two later findings: **CRIT-10** (per-source per-metric submission cooldown `MIN_SUBMISSION_INTERVAL = 1 hour`) and **HIGH-13** (weighted mean using `sources[src].weight`). The `_aggregate` function computes a plain unweighted mean, making the `weight` field in `SourceInfo` cosmetic only.

### Key Difference vs §35

```solidity
// MISSING in this version:
// uint256 public constant MIN_SUBMISSION_INTERVAL = 1 hours;
// mapping(address => mapping(bytes32 => uint256)) public lastSubmissionTime;

// submitMetric — NO cooldown check

// _aggregate — plain mean (not weighted):
uint256 sum = 0;
for (...) { sum += latestSourceValue[metricId][src]; }
uint256 mean = sum / activeCount; // unweighted
```

### Dependencies + Rebuild Recipe

```bash
forge build --contracts test_fixes/contracts/FlourishingMetricsOracleV3.sol
```

---

## 35. vf-audit-contracts/FlourishingMetricsOracleV3.sol

**Path:** `/home/user/workspace/vf-audit-contracts/FlourishingMetricsOracleV3.sol`
**Role:** Flourishing metrics oracle — Full V3 audit; CRIT-10 submission cooldown; HIGH-13 weighted mean; all fixes

### Summary

The full audit version. Adds `MIN_SUBMISSION_INTERVAL = 1 hour` (CRIT-10) and weighted mean aggregation (HIGH-13) on top of the base V3 features from §34. Sources now have meaningful `weight` fields that are respected in the mean calculation. The TWAP is updated using the weighted mean rather than the submitted raw value.

### Full Source

*(202 lines — full source read above.)*

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 19 | `MIN_SUBMISSION_INTERVAL = 1 hours` — CRIT-10. Prevents a single registered source from submitting rapidly to manipulate TWAP. |
| 64 | `lastSubmissionTime[address][bytes32] => uint256` — per-source per-metric cooldown tracker. |
| 99–103 | Cooldown check in `submitMetric`: `block.timestamp - lastSubmissionTime[msg.sender][metricId] >= MIN_SUBMISSION_INTERVAL`. Sets new timestamp after check. |
| 121–122 | `weightedSum += latestSourceValue[metricId][src] * w` and `totalWeight += w` — HIGH-13. |
| 144 | `mean = totalWeight > 0 ? weightedSum / totalWeight : weightedSum / activeCount` — weighted mean with plain-mean fallback for zero-weight sources. |
| 158 | TWAP uses `mean` (weighted) rather than raw `newValue`: `(m.twap * 3 + mean) / 4`. |

### Math

Weighted mean:
\[\text{mean} = \begin{cases} \frac{\sum_{i} \text{latestValue}[m][s_i] \times w_i}{\sum_i w_i} & \sum_i w_i > 0 \\ \frac{\sum_i \text{latestValue}[m][s_i]}{|\text{active}|} & \text{otherwise (all weights zero)} \end{cases}\]

Weighted EMA TWAP:
\[\text{TWAP}_n = \frac{3 \times \text{TWAP}_{n-1} + \text{mean}_n}{4}\]

Cooldown invariant:
\[\text{block.timestamp} - \text{lastSubmissionTime}[\text{src}][m] \ge 3600\]

### Events

Same as §33, plus the semantics of `MetricUpdated` now reflect the weighted mean instead of raw value.

### Security Invariants

1. Per-source cooldown prevents rapid TWAP manipulation (CRIT-10 fixed).
2. Weighted mean respects source trust tiers (HIGH-13 fixed).
3. TWAP uses weighted mean as input — manipulation requires distorting the entire weighted aggregate.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts vf-audit-contracts/FlourishingMetricsOracleV3.sol
```

---

## 36. v3-pr-ready/contracts/v3/FlourishingMetricsOracleV3.sol

**Path:** `/home/user/workspace/v3-pr-ready/contracts/v3/FlourishingMetricsOracleV3.sol`
**Role:** Flourishing metrics oracle — PR-ready V3; round-based model (different design from §33–§35); RoundStatus enum; deviation filtering; HIGH-2 staleness; MEDIUM-3 active round gate

### Summary

This is a **fundamentally different design** from §33–§35. While §33–§35 use a push-based continuous feed model, this version uses a **round-based model** (like V1 `FlourishingMetricsOracle`) with explicit round start/finalize lifecycle. It introduces a `RoundStatus` enum (`Open`, `Finalized`, `Expired`), deviation filtering with `_computeFilteredMedian`, HIGH-2 forward-only `latestValue` update, MEDIUM-1 `latestTimestamp`, and MEDIUM-3 `activeRoundForMetric` single-active-round gate. This is the most complete round-based V3 implementation.

### Full Source

*(485 lines — full source read above.)*

### Line-by-Line Walkthrough

| Lines | Description |
|-------|-------------|
| 86 | `enum RoundStatus { Open, Finalized, Expired }` — replaces `bool finalized`. |
| 119 | `DEVIATION_THRESHOLD = MAX_DEVIATION_BPS = 2000` (20%). |
| 147–150 | `latestTimestamp[bytes32]` — MEDIUM-1: finalization timestamp for staleness. `activeRoundForMetric[bytes32]` — MEDIUM-3: single active round gate. |
| 217–246 | `startRound(bytes32 metricId)` — checks `activeRoundForMetric[metricId]` for existing open round before creating new one. MEDIUM-3 fix. |
| 252–272 | `submitMetric(uint256 roundId, uint256 value)` — LOW-2: `value == 0` rejected. |
| 280–324 | `finalizeRound(uint256 roundId)` — two-pass filtering: initial median → filtered set → consensus. HIGH-1 + HIGH-2 + MEDIUM-1 fixes. |
| 297–300 | `_computeFilteredMedian` call — emits `DeviationFlagged` for outliers. |
| 316 | `if (roundId > latestRoundForMetric[round.metricId])` — HIGH-2: only updates `latestValue` if this is a newer round. Prevents stale round from overwriting fresh data. |
| 380–384 | `transferOwnership` — emits `OwnershipTransferred`. |
| 401–413 | `_insertionSort` — iterative, O(n²) worst case, safe for `MAXIMUM_ORACLES = 50`. |
| 418–430 | `_computeMedianFromValues` — sorts in-place, overflow-safe even-length formula. |
| 441–483 | `_computeFilteredMedian` — two-pass: count in-band values (emitting `DeviationFlagged` for each outlier), then collect and compute filtered median. |

### Math

Deviation band:
\[
\text{lower} = \text{refMedian} \times \frac{10000 - 2000}{10000} = \text{refMedian} \times 0.8
\]
\[
\text{upper} = \text{refMedian} \times \frac{10000 + 2000}{10000} = \text{refMedian} \times 1.2
\]

A submission \(v\) is in-band iff \(\text{lower} \le v \le \text{upper}\).

Overflow-safe median (same as §31):
\[
\text{median} = \frac{a}{2} + \frac{b}{2} + \frac{(a \bmod 2) + (b \bmod 2)}{2}
\]

### Events

| Event | When |
|-------|------|
| `OracleAdded(address oracle)` | `addOracle` |
| `OracleRemoved(address oracle)` | `removeOracle` |
| `RoundStarted(uint256 roundId, bytes32 metricId, uint256 deadline)` | `startRound` |
| `MetricSubmitted(uint256 roundId, address oracle, uint256 value)` | `submitMetric` |
| `ConsensusReached(uint256 roundId, bytes32 metricId, uint256 value, uint256 subCount)` | `finalizeRound` |
| `RoundExpired(uint256 roundId)` | Quorum failure in `finalizeRound` |
| `DeviationFlagged(uint256 roundId, address oracle, uint256 value, uint256 median)` | Outlier detection |
| `OwnershipTransferred(address prev, address next)` | `transferOwnership` |

### Security Invariants

1. Deviation filtering — outliers emitted with `DeviationFlagged` and excluded from consensus median.
2. Forward-only `latestValue` update (HIGH-2) — stale finalization cannot overwrite fresh data.
3. Single active round per metricId (MEDIUM-3) — no concurrent round spam.
4. `latestTimestamp` for staleness detection (MEDIUM-1).
5. Zero-value submissions rejected (LOW-2).
6. Insertion sort — O(n²) but bounded by `MAXIMUM_ORACLES = 50`, so maximum 2500 comparisons per sort.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts v3-pr-ready/contracts/v3/FlourishingMetricsOracleV3.sol
```

---

## 37. contracts/BaseOracle.sol

**Path:** `/home/user/workspace/contracts/BaseOracle.sol`
**Role:** Base oracle — CID and zkHash attestation emitter for live visualizations; mission evolution broadcaster

### Summary

`BaseOracle` is a lightweight, self-contained oracle contract with no proof verification logic. Its sole function is to emit events anchoring off-chain content to the Base chain: `emitAttestation` pins an IPFS CID and a ZK hash placeholder, returning a deterministic `keccak256(abi.encode(cid, zkHash, block.timestamp))` attestation ID. `publishMissionEvo` broadcasts a mission evolution — an ordered list of virtues and their weight multipliers — after a DAO vote. The guardian is an immutable address, preventing privilege escalation post-deployment.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  /// @title Vaultfire Base Oracle
 5  contract BaseOracle {
 6      address public immutable guardian;
 7      uint256 public constant RESONANCE_THRESHOLD_BPS = 5_100;
 8
 9      event VisualizationPinned(
10          address indexed guardian, string cid, string zkHash, uint256 timestamp
11      );
12      event MissionEvoBroadcast(
13          address indexed guardian,
14          bytes32 indexed evoId,
15          string[] virtues,
16          uint256[] weights,
17          uint256 thresholdBps,
18          uint256 timestamp
19      );
20
21      constructor(address guardianAddress) {
22          require(guardianAddress != address(0), "guardian_required");
23          guardian = guardianAddress;
24      }
25
26      modifier onlyGuardian() {
27          require(msg.sender == guardian, "guardian_only");
28          _;
29      }
30
31      function emitAttestation(string calldata cid, string calldata zkHash)
32          external onlyGuardian returns (bytes32 attestationId)
33      {
34          attestationId = keccak256(abi.encode(cid, zkHash, block.timestamp));
35          emit VisualizationPinned(msg.sender, cid, zkHash, block.timestamp);
36      }
37
38      function publishMissionEvo(string[] calldata virtues, uint256[] calldata weights)
39          external onlyGuardian returns (bytes32 evoId)
40      {
41          require(virtues.length == weights.length, "mission_length");
42          require(virtues.length != 0, "mission_empty");
43          evoId = keccak256(abi.encode(virtues, weights, block.timestamp));
44          emit MissionEvoBroadcast(msg.sender, evoId, virtues, weights, RESONANCE_THRESHOLD_BPS, block.timestamp);
45      }
46  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 6 | `guardian` — **immutable**. Set once in constructor; cannot be rotated. This prevents post-deployment guardian changes — a trade-off between simplicity and operational flexibility. |
| 7 | `RESONANCE_THRESHOLD_BPS = 5_100` — 51% threshold used in DAO governance quorum checks, stored here for downstream listeners to read. |
| 31–36 | `emitAttestation`: `keccak256(abi.encode(cid, zkHash, block.timestamp))` — uses `abi.encode` (not `encodePacked`) to prevent hash collision between two dynamic string parameters. The returned `attestationId` enables clients to reconcile the on-chain event with their off-chain record. |
| 38–45 | `publishMissionEvo`: length guard prevents empty or mismatched arrays. `abi.encode(virtues, weights, block.timestamp)` produces a deterministic `evoId` that anchors the mission evolution payload. The `RESONANCE_THRESHOLD_BPS` is included in the event for downstream DAO listeners. |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `guardian` | `address` | immutable | Only address allowed to emit attestations |
| `RESONANCE_THRESHOLD_BPS` | `uint256` | constant (5100) | DAO quorum threshold (51%) |

### Functions

**`emitAttestation(string cid, string zkHash) external onlyGuardian returns (bytes32)`**
- Computes attestation ID.
- Emits `VisualizationPinned`.
- Returns attestation ID for client reconciliation.

**`publishMissionEvo(string[] virtues, uint256[] weights) external onlyGuardian returns (bytes32)`**
- Validates lengths.
- Computes evoId.
- Emits `MissionEvoBroadcast` including `RESONANCE_THRESHOLD_BPS`.

### Math

Attestation ID:
\[\text{attestationId} = \text{keccak256}(\text{abi.encode}(\text{cid},\, \text{zkHash},\, \text{block.timestamp}))\]

Evolution ID:
\[\text{evoId} = \text{keccak256}(\text{abi.encode}(\text{virtues},\, \text{weights},\, \text{block.timestamp}))\]

Note: IDs are time-bound — two identical `(cid, zkHash)` pairs produce different IDs if submitted in different blocks, preventing collision.

### Events

| Event | When | Fields |
|-------|------|--------|
| `VisualizationPinned(address guardian, string cid, string zkHash, uint256 timestamp)` | `emitAttestation` | guardian indexed |
| `MissionEvoBroadcast(address guardian, bytes32 evoId, string[] virtues, uint256[] weights, uint256 thresholdBps, uint256 timestamp)` | `publishMissionEvo` | guardian and evoId indexed |

### Security Invariants

1. `guardian` is immutable — no privilege escalation post-deploy.
2. `abi.encode` prevents hash collision between dynamic string parameters.
3. No state storage (only events) — minimal attack surface.
4. `onlyGuardian` on both functions — no public write paths.

### ZK Proof Flow

Not applicable. This contract emits a `zkHash` string (an opaque reference to an off-chain ZK computation) but performs no on-chain proof verification.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts contracts/BaseOracle.sol
```

---

## 38. theloopbreaker-site/contracts/VaultfireTrustOracle.sol

**Path:** `/home/user/workspace/theloopbreaker-site/contracts/VaultfireTrustOracle.sol`
**Role:** Trust oracle — Off-chain performance attestations; quality score 0–10000; running average / EMA; task-linked; double-attestation guard; self-contained (no OZ imports)

### Summary

`VaultfireTrustOracle` records off-chain performance attestations submitted by trusted attesters about AI agents. Each attestation links a specific task ID to an agent address and records a quality score in the range [0, 10000]. The running average for an agent's score uses a true arithmetic mean for the first 1000 attestations, then switches to an EMA with coefficient 9/10 to give more weight to recent scores. A per-`(attester, agent, taskId)` triple guard prevents double-attestation. The contract is self-contained with no OpenZeppelin imports — uses custom role management.

### Full Source

*(Key sections — full source held in context.)*

```solidity
uint256 public constant MAX_QUALITY_SCORE  = 10000;
uint256 public constant EMA_SWITCH_COUNT   = 1000;
uint256 public constant EMA_SMOOTH_DENOM   = 10; // alpha = 1/10, smooth = 9/10

struct Attestation {
    address attester;
    address agent;
    bytes32 taskId;
    uint256 qualityScore;
    uint256 timestamp;
}

struct AgentStats {
    uint256 totalScore;
    uint256 attestationCount;
    uint256 averageScore;     // EMA or true average
    bool    exists;
}

mapping(address => AgentStats) public agentStats;
mapping(address => mapping(address => mapping(bytes32 => bool)))
    public hasAttested; // attester → agent → taskId → bool
mapping(uint256 => Attestation) public attestations;
uint256 public attestationCount;

address[] public attesters;
mapping(address => bool) public isAttester;
```

### Line-by-Line Walkthrough

| Function | Description |
|----------|-------------|
| `addAttester(address)` | Owner only; adds to `attesters` list and `isAttester` mapping. |
| `removeAttester(address)` | Owner only; removes from mapping (not from array — array grows unboundedly). |
| `submitAttestation(address agent, bytes32 taskId, uint256 qualityScore)` | Attester only; validates `qualityScore <= MAX_QUALITY_SCORE`, `agent != address(0)`, `taskId != 0`. Guards `hasAttested[msg.sender][agent][taskId]`. Updates `agentStats`. |
| `_updateAgentStats(address agent, uint256 score)` | Internal: if `count < EMA_SWITCH_COUNT`: `totalScore += score; avg = totalScore / count`. If `count >= EMA_SWITCH_COUNT`: `avg = (avg * 9 + score) / 10` (EMA). |

### Math

Running average (first 1000 attestations — true arithmetic mean):
\[\overline{s}_n = \frac{\sum_{i=1}^n s_i}{n}\]

EMA (after 1000 attestations — α = 1/10):
\[\overline{s}_n = \frac{9 \times \overline{s}_{n-1} + s_n}{10}\]

This is equivalent to a 10-period exponential moving average. The smooth factor (9/10) means each new score contributes 10% to the average. For a score well above the current average, the EMA responds gradually.

Quality score range: [0, 10000] where 10000 = perfect performance.

Double-attestation guard:
\[\text{key} = (\text{attester}, \text{agent}, \text{taskId})\]

The same `(attester, agent, taskId)` triple cannot be submitted twice.

### Events

| Event | When |
|-------|------|
| `AttestationSubmitted(uint256 attestationId, address attester, address agent, bytes32 taskId, uint256 score)` | `submitAttestation` |
| `AttesterAdded(address attester)` | `addAttester` |
| `AttesterRemoved(address attester)` | `removeAttester` |

### Security Invariants

1. Double-attestation guard keyed on `(attester, agent, taskId)` — same task cannot be scored twice by the same attester for the same agent.
2. `qualityScore > MAX_QUALITY_SCORE` rejected.
3. EMA switch at 1000 prevents early-epoch score manipulation from permanently distorting the long-run average.
4. `removeAttester` does not remove from the `attesters` array — only from `isAttester` mapping. The array grows over time (minor gas inefficiency).
5. No on-chain ZK verification — attester trustworthiness is social/economic, not cryptographic.
6. Self-contained (no OZ) — minimizes external dependency risk.

### ZK Proof Flow

Not applicable — attestations are off-chain and trust is based on attester authorization, not ZK proofs.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/contracts/VaultfireTrustOracle.sol
```

---

## 39. theloopbreaker-site/contracts/mocks/MockRiscZeroVerifier.sol

**Path:** `/home/user/workspace/theloopbreaker-site/contracts/mocks/MockRiscZeroVerifier.sol`
**Role:** Test mock — Minimal IRiscZeroVerifier for local Hardhat tests; configurable expected values; forced revert mode

### Summary

`MockRiscZeroVerifier` is a test double that implements the `IRiscZeroVerifier` interface from the mocks' parent directory. It stores `expectedImageId` and `expectedJournalDigest` and verifies that `verify` is called with those exact values. A `shouldRevert` flag allows tests to simulate proof failure. The `version()` function returns a fixed string. This mock is used in Foundry/Hardhat test suites to unit-test the verifier integration without requiring a real RISC Zero prover.

### Full Source

```solidity
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 3
 4  import "../IRiscZeroVerifier.sol";
 5
 6  contract MockRiscZeroVerifier is IRiscZeroVerifier {
 7      bytes32 public expectedImageId;
 8      bytes32 public expectedJournalDigest;
 9      bool    public shouldRevert;
10
11      function setExpected(bytes32 _imageId, bytes32 _journalDigest) external {
12          expectedImageId = _imageId;
13          expectedJournalDigest = _journalDigest;
14      }
15
16      function setShouldRevert(bool _shouldRevert) external {
17          shouldRevert = _shouldRevert;
18      }
19
20      function verify(
21          bytes calldata, /* proof */
22          bytes32 imageId,
23          bytes32 journalDigest
24      ) external view returns (bool) {
25          require(!shouldRevert, "Mock: forced revert");
26          require(imageId == expectedImageId, "Mock: bad imageId");
27          require(journalDigest == expectedJournalDigest, "Mock: bad journalDigest");
28          return true;
29      }
30
31      function version() external pure returns (string memory) {
32          return "mock-risc0-verifier";
33      }
34  }
```

### Line-by-Line Walkthrough

| Line | Description |
|------|-------------|
| 4 | `import "../IRiscZeroVerifier.sol"` — imports the local mock package's IRiscZeroVerifier (§4 variant — returns bool). |
| 7–9 | `expectedImageId`, `expectedJournalDigest`, `shouldRevert` — all configurable by test setup. |
| 11–14 | `setExpected(imageId, journalDigest)` — test setup function. Called in test `setUp()` or directly in test cases. No access control (test-only). |
| 16–18 | `setShouldRevert(bool)` — test setup for failure paths. |
| 20–29 | `verify` — `view` (not state-changing). Checks `shouldRevert` first (panic mode), then verifies `imageId` and `journalDigest` match expected values. Returns `true` on success. Proof bytes are ignored (`/* proof */`). |
| 31–33 | `version()` — returns `"mock-risc0-verifier"` for easy identification in test logs. |

### State Variables

| Variable | Type | Mutability | Purpose |
|----------|------|------------|---------|
| `expectedImageId` | `bytes32` | mutable (test) | Expected image ID for assertion |
| `expectedJournalDigest` | `bytes32` | mutable (test) | Expected journal digest for assertion |
| `shouldRevert` | `bool` | mutable (test) | Force revert mode for failure tests |

### Functions

**`setExpected(bytes32, bytes32) external`** — Configure expected values.

**`setShouldRevert(bool) external`** — Configure failure mode.

**`verify(bytes, bytes32, bytes32) external view returns (bool)`** — Mock implementation.

**`version() external pure returns (string memory)`** — Returns `"mock-risc0-verifier"`.

### Security Invariants

1. No access control — test-only contract. **MUST NOT be deployed to mainnet.**
2. `shouldRevert` mode causes the function to revert with `"Mock: forced revert"` — not return `false`. Callers using `try/catch` will enter the catch branch.
3. Proof bytes are completely ignored — any bytes pass verification as long as `imageId` and `journalDigest` match.

### Dependencies + Rebuild Recipe

```bash
forge build --contracts theloopbreaker-site/contracts/mocks/MockRiscZeroVerifier.sol
```

---

## 40. theloopbreaker-site/risc0/test/ProductionBeliefAttestationVerifier.t.sol

**Path:** `/home/user/workspace/theloopbreaker-site/risc0/test/ProductionBeliefAttestationVerifier.t.sol`
**Role:** Test suite — Foundry test for ProductionBeliefAttestationVerifier (§20); MockRiscZeroVerifier embedded; tests constructor, verifyProof, verifyAttestation paths

### Summary

Foundry test contract for `ProductionBeliefAttestationVerifier`. It embeds a `MockRiscZeroVerifier` (similar to §39) directly in the test file and exercises the full constructor, `verifyProof` (legacy 4-input path), and `verifyAttestation` (direct 6-field path). Tests cover: successful verification, proof failure (revert), invalid public inputs count, zero address rejection, zero image ID rejection, timelock mechanics (`proposeImageIdChange`, `executeImageIdChange` before/after delay, `cancelImageIdChange`), and ownership transfer.

### Full Source (Structure)

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "forge-std/Test.sol";
import "../src/ProductionBeliefAttestationVerifier.sol";
import "../interfaces/IRiscZeroVerifier.sol";

// Embedded mock verifier for tests
contract MockRiscZeroVerifier is IRiscZeroVerifier {
    bytes32 public expectedImageId;
    bytes32 public expectedJournalDigest;
    bool    public shouldRevert;

    function setExpected(bytes32 _imageId, bytes32 _journalDigest) external { ... }
    function setShouldRevert(bool v) external { shouldRevert = v; }
    function verify(bytes calldata, bytes32 imageId, bytes32 journalDigest) external {
        require(!shouldRevert, "Mock: forced revert");
        require(imageId == expectedImageId, "Mock: imageId mismatch");
        require(journalDigest == expectedJournalDigest, "Mock: digest mismatch");
        // No return value — uses Apache-2.0 revert-on-failure interface
    }
    function verifyIntegrity(Receipt calldata) external view {}
    function version() external pure returns (string memory) { return "mock"; }
}

contract ProductionBeliefAttestationVerifierTest is Test {
    ProductionBeliefAttestationVerifier verifier;
    MockRiscZeroVerifier mockRiscZero;

    bytes32 constant IMAGE_ID = bytes32(uint256(0xdeadbeef));

    function setUp() public {
        mockRiscZero = new MockRiscZeroVerifier();
        verifier = new ProductionBeliefAttestationVerifier(address(mockRiscZero), IMAGE_ID);
    }

    // Test: constructor validation
    function test_constructor_zeroVerifier() public { vm.expectRevert(ZeroAddress.selector); ... }
    function test_constructor_zeroImageId() public { vm.expectRevert(ZeroImageId.selector); ... }

    // Test: verifyProof (legacy 4-input path)
    function test_verifyProof_success() public {
        bytes32 beliefHash = keccak256("test_belief");
        address attester = address(0x1234);
        uint256 epoch = 1;
        uint256 moduleId = 42;

        bytes memory journal = abi.encode(beliefHash, attester, uint32(epoch), uint32(moduleId));
        bytes32 journalDigest = sha256(journal);
        mockRiscZero.setExpected(IMAGE_ID, journalDigest);

        uint256[] memory publicInputs = new uint256[](4);
        publicInputs[0] = uint256(beliefHash);
        publicInputs[1] = uint256(uint160(attester));
        publicInputs[2] = epoch;
        publicInputs[3] = moduleId;

        bool ok = verifier.verifyProof(hex"deadbeef", publicInputs);
        assertTrue(ok);
        assertTrue(verifier.hasAttestation(beliefHash, attester));
    }

    // Test: verifyAttestation (direct 6-field path)
    function test_verifyAttestation_success() public {
        bytes32 beliefHash = keccak256("belief");
        address attester = address(0xABCD);
        bytes memory journalData = abi.encode(
            beliefHash, attester,
            uint32(1), uint32(1), uint32(9000), uint64(block.timestamp)
        );
        bytes32 digest = sha256(journalData);
        mockRiscZero.setExpected(IMAGE_ID, digest);

        bool ok = verifier.verifyAttestation(hex"aabbcc", journalData);
        assertTrue(ok);
    }

    // Test: proof failure returns false
    function test_verifyAttestation_proofFailure_returnsFalse() public {
        mockRiscZero.setShouldRevert(true);
        bytes memory journal = abi.encode(...);
        bool ok = verifier.verifyAttestation(hex"aabbcc", journal);
        assertFalse(ok);
    }

    // Test: timelock
    function test_proposeAndExecuteImageIdChange() public {
        bytes32 newId = bytes32(uint256(0xcafebabe));
        verifier.proposeImageIdChange(newId);
        vm.expectRevert(TimelockNotExpired.selector);
        verifier.executeImageIdChange();
        vm.warp(block.timestamp + 48 hours + 1);
        verifier.executeImageIdChange();
        assertEq(verifier.imageId(), newId);
    }

    function test_cancelImageIdChange() public {
        bytes32 newId = bytes32(uint256(0xcafebabe));
        verifier.proposeImageIdChange(newId);
        verifier.cancelImageIdChange();
        assertEq(verifier.pendingImageId(), bytes32(0));
    }
}
```

### Line-by-Line Walkthrough

| Test | What It Verifies |
|------|-----------------|
| `test_constructor_zeroVerifier` | `ZeroAddress` revert when `_riscZeroVerifier == address(0)`. |
| `test_constructor_zeroImageId` | `ZeroImageId` revert when `_imageId == bytes32(0)`. |
| `test_verifyProof_success` | Full happy path: journal reconstructed as 4-field, digest matches, `hasAttestation` set, returns `true`. |
| `test_verifyProof_wrongInputCount` | `InvalidPublicInputsCount` revert for arrays ≠ 4 elements. |
| `test_verifyAttestation_success` | 6-field journal decoded, `hasAttestation` set, `attestationCount` incremented. |
| `test_verifyAttestation_proofFailure_returnsFalse` | `shouldRevert = true` → mock reverts → `verifyAttestation` returns `false`. |
| `test_proposeAndExecuteImageIdChange` | Timelock: execute before 48h reverts; after 48h succeeds; `imageId` updated. |
| `test_cancelImageIdChange` | Cancellation clears `pendingImageId` and `pendingImageIdEffectiveAt`. |
| `test_transferOwnership` | Ownership transferred; old owner loses access. |

### Key Technical Notes

The embedded `MockRiscZeroVerifier` in this file implements the **Apache-2.0 revert-on-failure** interface (no return value from `verify`), matching the `IRiscZeroVerifier` at §5. This is distinct from the `MockRiscZeroVerifier` at §39 which returns `bool`. The `theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol` uses `try riscZeroVerifier.verify(...)` without capturing a return value — consistent with the revert-on-failure interface.

### Security Invariants Tested

1. Constructor zero-address guards.
2. Journal digest construction using `sha256` (not `keccak256`).
3. 4-element vs 6-element input count validation.
4. Silent `false` return on proof failure (via `try/catch`).
5. 48-hour timelock enforcement.
6. Single pending proposal guard (`ImageIdAlreadyPending`).

### Dependencies + Rebuild Recipe

```bash
forge test --match-path theloopbreaker-site/risc0/test/ProductionBeliefAttestationVerifier.t.sol -vvv
```

---

## Cross-Contract Compatibility Matrix

The following table maps each attestor variant to compatible verifier variants based on `PUBLIC_INPUTS_COUNT`:

| Attestor | publicInputs length | Compatible Verifiers |
|----------|--------------------|--------------------|
| DilithiumAttestor V1 (§7) | 4 | ProductionBeliefAttestationVerifier V2 (§14, §15), BeliefAttestationVerifierProduction (§14), BeliefAttestationVerifier DEV (§13), risc0/src/Production (§20) |
| DilithiumAttestor mini-app (§8) | N/A (direct IRiscZeroVerifier) | Directly calls riscZeroVerifier — not IStarkVerifier |
| DilithiumAttestorV3 v3/ staging (§10) | 4 | Same as V1 verifiers |
| DilithiumAttestorV3 v3-pr-ready (§11) | 4 | Same as V1 verifiers |
| DilithiumAttestorV3 vf-audit / push (§9, §12) | **6** | ProductionBeliefAttestationVerifierV3 vf-audit (§16), push version (§19) |

**Critical incompatibility:** Pairing `DilithiumAttestorV3` (vf-audit, §9) with `ProductionBeliefAttestationVerifierV3` from the `/v3/` or `/v3-pr-ready/` staging directories (§17, §18) will cause all proof submissions to revert with `InvalidPublicInputsCount` because those staging verifiers expect 4 inputs but receive 6.

---

## Dilithium Attestation — Quantum Resistance Context

The name "Dilithium" refers to the CRYSTALS-Dilithium lattice-based digital signature scheme, a NIST post-quantum cryptography standard (FIPS 204, 2024). However, in the Vaultfire codebase, the `DilithiumAttestor` contracts do not implement the Dilithium lattice signature scheme on-chain. Instead they:

1. Use ECDSA (classical elliptic curve) for the off-chain signer authorization check.
2. Delegate quantum-relevant computation to the RISC Zero zkVM guest program.
3. The "Dilithium" name signals **intent** — the system architecture positions the RISC Zero guest as the location where post-quantum Dilithium signature verification would be implemented, since the zkVM can execute arbitrary Rust code including CRYSTALS-Dilithium libraries (`pqcrypto` crate).

**What CRYSTALS-Dilithium provides:**
- Module-lattice-based signatures (MLWE/MSIS security assumptions).
- Security relies on the hardness of the Module Learning With Errors problem, believed resistant to Shor's algorithm.
- Signature sizes: ~2420 bytes (Dilithium2), ~3293 bytes (Dilithium3), ~4595 bytes (Dilithium5).
- Verification time: ~microseconds on modern hardware.

**Why it matters for the architecture:**
If ECDSA (secp256k1) is broken by a sufficiently powerful quantum computer, the `signer` authorization check in `DilithiumAttestor` contracts would be compromised. Moving the signer check into the zkVM guest (running Dilithium) and verifying the Groth16-wrapped STARK on-chain would make the entire attestation pipeline quantum-resistant without changing the on-chain Solidity code.

**Current state:** The Vaultfire contracts use ECDSA for the signer step and RISC Zero Groth16 for the ZK step. The Groth16 proof system itself relies on bilinear pairings (BN254 curve) which are also vulnerable to quantum attacks. A fully quantum-resistant deployment would require switching to a post-quantum SNARK scheme (e.g., FRI-based STARKs with appropriate hash functions, or lattice-based proofs). This is an acknowledged future roadmap item.

---

## Version Lineage Summary

```
V1 Family:
  contracts/DilithiumAttestor.sol          (global mapping, no msg.sender bind)
  contracts/BeliefOracle.sol               (global bonusApplied, unbounded batch)
  contracts/MultiOracleConsensus.sol       (recursive sort, O(n) dup check)
  contracts/FlourishingMetricsOracle.sol   (round-based, no deviation filter)
  contracts/ProductionBeliefAttestationVerifier.sol  (4 inputs, no timelock)

V2 Family:
  contracts/BeliefAttestationVerifierProduction.sol  (sha256 fix, immutable, no timelock)
  contracts/ProductionBeliefAttestationVerifier.sol  (4 inputs, mutable imageId, no timelock)

V3 Staging:
  v3/DilithiumAttestorV3.sol               (per-user, msg.sender bind, 4 inputs)
  v3/BeliefOracleV3.sol                    (per-user bonus, MIN_BELIEF_COUNT, CEI)
  v3/ProductionBeliefAttestationVerifierV3.sol  (4 inputs — mismatch with audit)

V3 PR-Ready:
  v3-pr-ready/contracts/v3/DilithiumAttestorV3.sol      (4 inputs, different import)
  v3-pr-ready/contracts/v3/BeliefOracleV3.sol            (bonusApplied before try — HIGH-01 unfixed)
  v3-pr-ready/contracts/v3/MultiOracleConsensusV3.sol    (all HIGH/MEDIUM fixes)
  v3-pr-ready/contracts/v3/FlourishingMetricsOracleV3.sol (round-based, full deviation filter)

V3 Audit (vf-audit-contracts/):
  DilithiumAttestorV3.sol       ← 6 inputs, full CRIT-07 fix
  BeliefOracleV3.sol            ← HIGH-01 + HIGH-21 two-step guardian
  MultiOracleConsensusV3.sol    ← commit-reveal, all fixes
  FlourishingMetricsOracleV3.sol ← CRIT-10 cooldown, HIGH-13 weighted mean
  ProductionBeliefAttestationVerifierV3.sol ← 6 inputs, epoch-aware, timelock, revert

V3 Final Push (push_contracts_v3_*):
  DilithiumAttestorV3.sol       ← same as audit
  BeliefOracleV3.sol            ← same as audit (minus two-step guardian)
  MultiOracleConsensusV3.sol    ← same as audit
  FlourishingMetricsOracleV3.sol ← same as audit
  ProductionBeliefAttestationVerifierV3.sol ← same as audit

Standalone / Alternative:
  theloopbreaker-site/risc0/src/ProductionBeliefAttestationVerifier.sol
    ← 4 inputs, 6-field verifyAttestation, 48hr timelock, returns false on catch
  theloopbreaker-site/base-mini-app/contracts/DilithiumAttestor.sol
    ← direct IRiscZeroVerifier, zkEnabled toggle, per-user timestamp
  contracts/BaseOracle.sol
    ← CID/zkHash event emitter, immutable guardian
  theloopbreaker-site/contracts/VaultfireTrustOracle.sol
    ← off-chain performance attestations, EMA score
  contracts/BeliefAttestationVerifier.sol
    ← DEV ONLY, chain ID guard
```

---

*End of Vaultfire Codex — Batch 4: Belief / Attestation / Oracle / ZK-Proof Contracts*

*Document covers 40 contracts. Total contracts audited: interfaces (6), attestors (6), verifiers (8), belief oracles (5), multi-oracle consensus (5), flourishing metrics oracles (6), base oracles (2), trust oracle (1), mocks/tests (2).*


<div style="page-break-before:always"></div>

# VaultfireKeyProtocol (VKP) — Technical Reference

**Codex Batch 5 — 17 Contracts**
**Solidity** `^0.8.26` · **License** MIT · **Network** Base / Avalanche / Arbitrum / Polygon

---

## Table of Contents

1. [VKP Architecture Overview](#vkp-architecture-overview)
2. [IVaultfireIdentity.sol](#1-ivaultfireidentitysol)
3. [IVaultfireBonds.sol](#2-ivaultfirebondssol)
4. [IVaultfireReputation.sol](#3-ivaultfirereputationsol)
5. [KeyVault.sol](#4-keyvaultsol)
6. [VKPFactory.sol](#5-vkpfactorysol)
7. [SessionKeyManager.sol](#6-sessionkeymanagersol)
8. [ScopedDelegation.sol](#7-scopeddelegationsol)
9. [DelegationRegistry.sol](#8-delegationregistrysol)
10. [AgentSLAEnforcer.sol](#9-agentslaenforcersol)
11. [AgentInsurancePool.sol](#10-agentinsurancepoolsol)
12. [AgentSafetyManager.sol](#11-agentsafetymanagersol)
13. [ComplianceRegistry.sol](#12-complianceregistrysol)
14. [ReputationDecay.sol](#13-reputationdecaysol)
15. [MockIdentityRegistry.sol](#14-mockidentityregistrysol)
16. [MockBondsRegistry.sol](#15-mockbondsregistrysol)
17. [MockReputationRegistry.sol](#16-mockreputationregistrysol)
18. [ReentrancyAttacker.sol](#17-reentrancyattackersol)
19. [Math Reference](#math-reference)
20. [Security Invariants](#security-invariants)
21. [Dependencies and Rebuild Recipe](#dependencies-and-rebuild-recipe)

---

## VKP Architecture Overview

The VaultfireKeyProtocol is a layered system of eight production contracts, three interfaces, three testing mocks, and one adversarial test helper, all operating at `^0.8.26`. Its purpose is to give every AI agent registered through ERC-8004 a complete on-chain trust stack: a spending-policy-enforced wallet, ephemeral session keys, delegation chains that can only narrow (never widen) permissions, SLA commitments backed by staked collateral, a pooled insurance mechanism, an automated kill switch tied to reputation scores, a regulatory compliance registry for EU AI Act attestations, and a time-decay model that prevents stale reputation from gaming the system.

### Layered Architecture

```
┌──────────────────────────────────────────────────────────┐
│                    VKPFactory                            │
│  (one-call deployer of per-agent KeyVaults)              │
└────────────────────┬─────────────────────────────────────┘
                     │ deploys
                     ▼
┌──────────────────────────────────────────────────────────┐
│                    KeyVault                              │
│  Policy: maxPerTx · maxHourly · maxDaily · emergFreeze   │
│  Allowlist: contract targets · emergency controls        │
└────┬───────────────┬──────────────────────────────────────┘
     │               │
     │ validated by  │ validated by
     ▼               ▼
┌───────────────┐ ┌──────────────────────────────────────┐
│ SessionKey    │ │ DelegationRegistry / ScopedDelegation │
│ Manager       │ │  (agent-to-agent authority chains)    │
└───────────────┘ └──────────────────────────────────────┘
     │                        │
     └──────────┬─────────────┘
                │ both depend on
                ▼
┌──────────────────────────────────────────────────────────┐
│              Trust Layer                                 │
│  AgentSLAEnforcer · AgentInsurancePool                   │
│  AgentSafetyManager · ComplianceRegistry · ReputDecay    │
└──────────────────────────────────────────────────────────┘
                │
                │ all consult
                ▼
┌──────────────────────────────────────────────────────────┐
│              Shared Interfaces                           │
│  IVaultfireIdentity (ERC-8004) · IVaultfireBonds         │
│  IVaultfireReputation                                    │
└──────────────────────────────────────────────────────────┘
```

### Flow: Agent Acts as User

1. An AI agent is registered on-chain via ERC-8004 (`IVaultfireIdentity`).
2. The agent (or the Factory on its behalf) deploys a `KeyVault` — a personal spending-policy enforcement contract.
3. For a scoped task, the agent creates a `SessionKey` in `SessionKeyManager`, granting a short-lived address only the minimum authority needed.
4. If the agent needs to sub-contract work, it creates a `Scope` in `ScopedDelegation` or a full `Delegation` in `DelegationRegistry`, both of which enforce progressive narrowing of permissions.
5. `DelegationRegistry` re-verifies delegate trust live against bond balance and reputation score; if either drops below threshold, the delegation auto-revokes.
6. `AgentSLAEnforcer` enforces the agent's public performance commitments; violations slash the staked collateral.
7. `AgentInsurancePool` provides third-party coverage — premiums priced by reputation tier.
8. `AgentSafetyManager` acts as the kill switch layer; `ReputationDecay` feeds live decayed scores back into this layer and into insurance tier pricing.
9. `ComplianceRegistry` records EU AI Act attestations, maintaining a regulatory audit trail that external auditors can verify on-chain.

---

## 1. IVaultfireIdentity.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireIdentity.sol`
**Role:** Interface — ERC-8004 identity gate used by every VKP contract to verify agent registration.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireIdentity — Interface for the Vaultfire Identity Registry
     5  /// @notice Minimal interface used by VKP to verify ERC-8004 agent registration.
     6  ///         Uses getAgent() to check registration — returns agentURI + metadata.
     7  ///         An agent is registered if getAgent() returns a non-empty agentURI.
     8  interface IVaultfireIdentity {
     9      /// @notice Get an agent's on-chain identity data
    10      /// @dev Returns empty agentURI if the agent is not registered.
    11      /// @param agentAddress The agent address to look up
    12      /// @return agentURI The agent's URI (JSON metadata string)
    13      /// @return active Whether the agent is active
    14      /// @return agentType The agent type string
    15      /// @return registeredAt Block number when registered
    16      function getAgent(address agentAddress)
    17          external
    18          view
    19          returns (
    20              string memory agentURI,
    21              bool active,
    22              string memory agentType,
    23              uint256 registeredAt
    24          );
    25  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 1–2 | MIT license; Solidity minimum 0.8.26 required. |
| 4–7 | NatSpec describes the registration check convention: a non-empty `agentURI` means registered. |
| 8 | `interface` keyword — no storage, no constructor. |
| 9–24 | Single function `getAgent(address)`. Returns four values; callers check `bytes(agentURI).length > 0` for registration. |

### Functions

#### `getAgent(address agentAddress) → (string agentURI, bool active, string agentType, uint256 registeredAt)`
- **Visibility:** `external view`
- **Purpose:** Single source of truth for ERC-8004 registration status. Every VKP contract calls this in a modifier or constructor guard. The `active` flag is consulted only in contracts where an inactive agent should be treated differently (e.g., `ScopedDelegation`, `AgentSLAEnforcer`); `KeyVault` only checks `agentURI` length.
- **Return convention:** If the address is not registered, `agentURI` is an empty string and all other fields are zero/false/empty.

### Security Invariants

- VKP registration checks are read-only calls to an external registry. A malicious registry can return `true` for any address. Production deployments must use the canonical ERC-8004 registry at the well-known deployment address.
- Empty-string check (`bytes(agentURI).length == 0`) is the universal registration sentinel across all VKP contracts — consistent but does not verify the URI is well-formed JSON.

---

## 2. IVaultfireBonds.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireBonds.sol`
**Role:** Interface — live bond registry ABI for cross-chain accountability verification (Base, Avalanche, Arbitrum, Polygon).

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireBonds — Interface for Vaultfire Accountability Bond contracts
     5  /// @notice Matches the LIVE ABIs on Base, Avalanche, Arbitrum, and Polygon.
     6  ///         Bonds are looked up by bondId. The bond struct includes the creator
     7  ///         (aiCompany) address, which VKP uses to verify the delegate owns the bond.
     8  interface IVaultfireBonds {
     9      /// @notice Get bond details by ID
    10      /// @dev Matches the live getBond(uint256) on all 4 chains
    11      function getBond(uint256 bondId)
    12          external
    13          view
    14          returns (
    15              address creator,        // aiCompany / bond creator address
    16              uint256 stakeAmount,
    17              uint256 createdAt,
    18              bool active
    19          );
    20  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 4–7 | Declares that this ABI must match the live bonds contract on four networks — a cross-chain compatibility contract. |
| 11–19 | `getBond(uint256)` returns the bond creator (`aiCompany`), the staked amount in wei, creation timestamp, and whether the bond is active. |

### Functions

#### `getBond(uint256 bondId) → (address creator, uint256 stakeAmount, uint256 createdAt, bool active)`
- **Visibility:** `external view`
- **Used by:** `DelegationRegistry.acceptDelegation()` and `_verifyTrustRequirements()`, `AgentSLAEnforcer` constructor (storage).
- **Key usage pattern:** Callers verify `creator == delegate && active && stakeAmount >= minRequired`.

### Security Invariants

- The bond check in `DelegationRegistry` is wrapped in a `try/catch` (see §8). If the call reverts (e.g., bond ID does not exist), the delegation is auto-revoked rather than leaving it active — fail-closed design.
- `stakeAmount` is a snapshot at query time; nothing prevents a delegate from withdrawing stake after acceptance. The `verifyTrust()` function exists to detect this and revoke in-flight delegations.

---

## 3. IVaultfireReputation.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireReputation.sol`
**Role:** Interface — reputation score feed used for delegation gating, insurance pricing, and safety auto-triggers.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireReputation — Interface for Vaultfire Reputation Registry
     5  /// @notice Used by VKP to check reputation scores for delegation gating
     6  interface IVaultfireReputation {
     7      function getReputation(address agent)
     8          external
     9          view
     10         returns (uint256 score, uint256 totalRatings, uint256 lastUpdated);
    11  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 6–11 | Minimal interface: one function, three return values. `score` is the canonical 0–100 trust score. |

### Functions

#### `getReputation(address agent) → (uint256 score, uint256 totalRatings, uint256 lastUpdated)`
- **score:** 0–100 trust score. 0 = untrusted, 100 = maximum trust.
- **totalRatings:** lifetime rating count (used by `ReputationDecay` for context).
- **lastUpdated:** timestamp — `ReputationDecay` uses this to seed its own `lastActivityAt`.

### Security Invariants

- Reputation calls are also wrapped in `try/catch` in `DelegationRegistry._verifyTrustRequirements()` — a failing external call cannot keep a delegation alive.
- `AgentInsurancePool` reads reputation only at policy-creation time to compute the risk tier; it does not re-check during the policy lifecycle. An agent can improve or worsen their reputation after purchasing insurance without policy repricing.

---

## 4. KeyVault.sol

**File:** `vaultfire-keyprotocol/contracts/KeyVault.sol`
**Role:** KeyVault — per-agent spending-policy enforcement contract.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5
     6  /// @title KeyVault — On-chain spending policy enforcement for AI agent wallets
     7  /// @author Vaultfire Protocol
     8  /// @notice Wraps an agent's wallet with enforced spending limits, contract
     9  ///         allowlists, and emergency freeze. Policies are on-chain — no prompt
    10  ///         injection or client-side bypass can override them.
     11 /// @dev Requires the owner to be registered in the Vaultfire Identity Registry (ERC-8004).
    12  contract KeyVault {
    13      // ═══ REENTRANCY GUARD ═══
    14
    15      uint256 private constant _NOT_ENTERED = 1;
    16      uint256 private constant _ENTERED = 2;
    16      uint256 private _reentrancyStatus = _NOT_ENTERED;
    17
    18      modifier nonReentrant() {
    19          require(_reentrancyStatus != _ENTERED, "ReentrancyGuard: reentrant call");
    20          _reentrancyStatus = _ENTERED;
    21          _;
    22          _reentrancyStatus = _NOT_ENTERED;
    23      }
    24
    25      // ═══ STRUCTS ═══
    26
    27      struct Policy {
    28          uint256 maxPerTx;           // Max native token per single transaction (wei)
    29          uint256 maxDaily;           // Max native token per rolling 24h window (wei)
    30          uint256 maxHourly;          // Max native token per rolling 1h window (wei)
    31          uint256 emergencyThreshold; // Freeze if total spent exceeds this (wei)
    32          bool frozen;                // Emergency freeze flag
    33      }
    34
    35      struct SpendingWindow {
    36          uint256 hourlySpent;
    37          uint256 dailySpent;
    38          uint256 totalSpent;
    39          uint256 hourlyResetAt;
    40          uint256 dailyResetAt;
    41      }
    42
    43      // ═══ STATE ═══
    44
    45      address public immutable owner;
    46      IVaultfireIdentity public immutable identityRegistry;
    47
    48      Policy public policy;
    49      SpendingWindow public spending;
    50
    51      mapping(address => bool) public allowedContracts;
    52      uint256 public allowedContractCount;
    53
    54      bool public allowlistEnabled;
    55
    56      // ═══ EVENTS ═══
    57
    58      event VaultCreated(address indexed owner, uint256 maxDaily, uint256 maxPerTx);
    59      event TransactionExecuted(address indexed to, uint256 value, bytes data);
    60      event PolicyUpdated(uint256 maxPerTx, uint256 maxDaily, uint256 maxHourly);
    61      event ContractAllowed(address indexed target);
    62      event ContractRemoved(address indexed target);
    63      event EmergencyFreeze(uint256 totalSpent, uint256 threshold);
    64      event EmergencyUnfreeze(address indexed owner);
    65      event FundsDeposited(address indexed from, uint256 amount);
    66      event FundsWithdrawn(address indexed to, uint256 amount);
    67
    68      // ═══ ERRORS ═══
    69
    70      error NotOwner();
    71      error NotRegistered();
    72      error VaultFrozen();
    73      error ExceedsPerTxLimit(uint256 requested, uint256 limit);
    74      error ExceedsHourlyLimit(uint256 wouldSpend, uint256 limit);
    75      error ExceedsDailyLimit(uint256 wouldSpend, uint256 limit);
    76      error ContractNotAllowed(address target);
    77      error TransactionFailed();
    78      error ZeroAddress();
    79
    80      // ═══ MODIFIERS ═══
    81
    82      modifier onlyOwner() {
    83          if (msg.sender != owner) revert NotOwner();
    84          _;
    85      }
    86
    87      modifier notFrozen() {
    88          if (policy.frozen) revert VaultFrozen();
    89          _;
    90      }
    91
    92      // ═══ CONSTRUCTOR ═══
    93
    94      constructor(
    95          address _owner,
    96          address _identityRegistry,
    97          uint256 _maxPerTx,
    98          uint256 _maxHourly,
    99          uint256 _maxDaily,
    100         uint256 _emergencyThreshold
    101     ) {
    102         if (_owner == address(0)) revert ZeroAddress();
    103         if (_identityRegistry == address(0)) revert ZeroAddress();
    104
    105         identityRegistry = IVaultfireIdentity(_identityRegistry);
    106
    107         // Verify owner is a registered ERC-8004 agent
    108         (string memory agentURI,,,) = identityRegistry.getAgent(_owner);
    109         if (bytes(agentURI).length == 0) revert NotRegistered();
    110
    110         owner = _owner;
    111         policy = Policy({
    112             maxPerTx: _maxPerTx,
    113             maxDaily: _maxDaily,
    114             maxHourly: _maxHourly,
    115             emergencyThreshold: _emergencyThreshold,
    116             frozen: false
    117         });
    118
    119         spending = SpendingWindow({
    120             hourlySpent: 0,
    120             dailySpent: 0,
    121             totalSpent: 0,
    122             hourlyResetAt: block.timestamp + 1 hours,
    123             dailyResetAt: block.timestamp + 1 days
    124         });
    125
    126         allowlistEnabled = false;
    127         emit VaultCreated(_owner, _maxDaily, _maxPerTx);
    128     }
    129
    130     // ═══ RECEIVE / DEPOSIT ═══
    131
    132     receive() external payable {
    133         emit FundsDeposited(msg.sender, msg.value);
    134     }
    135
    136     function deposit() external payable {
    137         emit FundsDeposited(msg.sender, msg.value);
    138     }
    139
    140     // ═══ EXECUTE ═══
    141
    142     function execute(
    143         address to,
    144         uint256 value,
    145         bytes calldata data
    146     ) external onlyOwner notFrozen nonReentrant returns (bytes memory result) {
    147         if (to == address(0)) revert ZeroAddress();
    148
    149         if (allowlistEnabled && !allowedContracts[to]) {
    150             revert ContractNotAllowed(to);
    151         }
    152
    153         if (value > policy.maxPerTx) {
    154             revert ExceedsPerTxLimit(value, policy.maxPerTx);
    155         }
    156
    157         _resetWindows();
    158
    159         if (spending.hourlySpent + value > policy.maxHourly) {
    160             revert ExceedsHourlyLimit(spending.hourlySpent + value, policy.maxHourly);
    160         }
    161
    162         if (spending.dailySpent + value > policy.maxDaily) {
    163             revert ExceedsDailyLimit(spending.dailySpent + value, policy.maxDaily);
    164         }
    165
    166         spending.hourlySpent += value;
    167         spending.dailySpent += value;
    168         spending.totalSpent += value;
    169
    170         if (policy.emergencyThreshold > 0 && spending.totalSpent > policy.emergencyThreshold) {
    171             policy.frozen = true;
    172             emit EmergencyFreeze(spending.totalSpent, policy.emergencyThreshold);
    173         }
    174
    175         emit TransactionExecuted(to, value, data);
    176
    177         bool success;
    178         (success, result) = to.call{value: value}(data);
    179         if (!success) revert TransactionFailed();
    180     }
    181
    182     // ═══ POLICY MANAGEMENT ═══
    183
    184     function updatePolicy(uint256 _maxPerTx, uint256 _maxHourly, uint256 _maxDaily,
    185         uint256 _emergencyThreshold) external onlyOwner { ... }
    186
    186     function allowContract(address target) external onlyOwner { ... }
    187     function removeContract(address target) external onlyOwner { ... }
    188     function disableAllowlist() external onlyOwner { ... }
    189     function enableAllowlist() external onlyOwner { ... }
    190
    191     // ═══ EMERGENCY ═══
    192
    192     function freeze() external onlyOwner { ... }
    193     function unfreeze() external onlyOwner { ... }
    194     function emergencyWithdraw() external onlyOwner nonReentrant { ... }
    195
    196     // ═══ VIEW ═══
    197
    196     function remainingHourly() external view returns (uint256) { ... }
    197     function remainingDaily() external view returns (uint256) { ... }
    198     function isFrozen() external view returns (bool) { ... }
    199     function balance() external view returns (uint256) { ... }
    200
    201     // ═══ INTERNAL ═══
    202
    202     function _resetWindows() internal { ... }
    203 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 15–26 | **Reentrancy guard** — inline implementation. `_NOT_ENTERED = 1`, `_ENTERED = 2`. Uses uint256 rather than bool to prevent gas optimisation that could create slot collision vulnerabilities. The `nonReentrant` modifier sets status to `_ENTERED` before body and resets after. |
| 27–33 | **`Policy` struct** — all spending policy fields. `frozen` flag doubles as the kill switch. `emergencyThreshold` is a lifetime total, not a window. |
| 35–41 | **`SpendingWindow` struct** — rolling window accumulators. `totalSpent` is the monotonically increasing lifetime total. |
| 45–46 | `owner` and `identityRegistry` are `immutable` — set in constructor, cannot be changed. No ownership transfer is possible on `KeyVault`. |
| 51–54 | `allowedContracts` mapping + `allowedContractCount` counter + `allowlistEnabled` flag. The count is used to auto-disable the allowlist when the last contract is removed. |
| 82–90 | `onlyOwner` uses a custom error `NotOwner()` (cheaper than a string revert). `notFrozen` checks `policy.frozen`. |
| 102–109 | Constructor: zero-address checks come first, then the ERC-8004 registration check. `getAgent()` is called with `_owner`; if the returned URI is empty, the constructor reverts — an unregistered address cannot own a `KeyVault`. |
| 119–127 | Spending windows initialized: hourly resets one hour from now, daily resets one day from now. `allowlistEnabled = false` — open by default. |
| 142–180 | **`execute()`** — the core enforcement function. Modifier order: `onlyOwner notFrozen nonReentrant`. Checks run in this order: (1) zero-address target, (2) allowlist gate, (3) per-tx limit, (4) window reset, (5) hourly limit, (6) daily limit. Effects (state updates) happen before the external call (CEI pattern). The `TransactionExecuted` event is emitted before `to.call` — an unusual pattern but not a security issue since the event is purely informational. |
| 166–168 | Spending accumulators updated before the external call. If `_resetWindows()` already zeroed a window, the fresh window starts accumulating with this transaction's value. |
| 170–173 | Emergency auto-freeze: after spending update, if `totalSpent > emergencyThreshold` and the threshold is non-zero, `policy.frozen` is set to `true` and `EmergencyFreeze` is emitted — all future `execute()` calls will revert with `VaultFrozen`. The current transaction still completes. |
| 232–265 | **`updatePolicy()`** — all four limits can be changed atomically. Note: increasing limits does not reset window counters; an agent could increase `maxDaily` mid-day to allow more spending. |
| 246–254 | **`allowContract()`** — idempotent. Only adds if not already present; sets `allowlistEnabled = true` upon first addition. |
| 257–266 | **`removeContract()`** — decrements count; when count reaches zero, auto-disables allowlist (open-world execution). |
| 283–286 | **`freeze()`** — manual kill switch. Emits `EmergencyFreeze` with current `totalSpent` and configured threshold for audit trail. |
| 289–293 | **`unfreeze()`** — resets `totalSpent` to zero so the emergency threshold accumulator starts fresh. |
| 296–301 | **`emergencyWithdraw()`** — full drain, `nonReentrant`. Emits `FundsWithdrawn` before the call (CEI). |
| 308–319 | View functions return snapshots: `remainingHourly()/remainingDaily()` check whether the window has already expired (returning full limit) without mutating state. |
| 335–344 | **`_resetWindows()`** — if `block.timestamp >= hourlyResetAt`, zeroes `hourlySpent` and advances `hourlyResetAt` by one hour. Same for daily. This is a fixed-point reset: it does not roll the window forward by one period per call — it always resets to `now + period`. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `owner` | `address immutable` | ERC-8004 agent that owns and controls this vault | Immutable |
| `identityRegistry` | `IVaultfireIdentity immutable` | Registry used for owner registration check at deploy time | Immutable |
| `policy` | `Policy storage` | Active spending policy including frozen flag | Mutable (owner) |
| `spending` | `SpendingWindow storage` | Rolling window accumulators | Mutable (execute path) |
| `allowedContracts` | `mapping(address => bool)` | Per-target allowlist | Mutable (owner) |
| `allowedContractCount` | `uint256` | Count of allowlisted contracts | Mutable (owner) |
| `allowlistEnabled` | `bool` | Master switch for allowlist enforcement | Mutable (owner) |
| `_reentrancyStatus` | `uint256 private` | Reentrancy guard state (1 = not entered, 2 = entered) | Mutable (execute path) |

### Functions

#### `execute(address to, uint256 value, bytes calldata data) → bytes result`
- **Modifiers:** `onlyOwner`, `notFrozen`, `nonReentrant`
- **Checks sequence:** zero address → allowlist → per-tx limit → window reset → hourly accumulator → daily accumulator → emergency threshold
- **Effects sequence:** hourly/daily/total spending updated → optional auto-freeze → event emitted
- **Interaction:** `to.call{value: value}(data)` — if this fails, reverts with `TransactionFailed()`

#### `updatePolicy(uint256, uint256, uint256, uint256)`
- **Modifier:** `onlyOwner`
- All four policy fields updated atomically. Does not reset window counters.

#### `allowContract(address target)` / `removeContract(address target)`
- **Modifier:** `onlyOwner`
- Idempotent add/remove. Auto-enables/disables allowlist by count.

#### `freeze()` / `unfreeze()`
- **Modifier:** `onlyOwner`
- `freeze()`: sets `policy.frozen = true`. `unfreeze()`: sets `false` AND resets `spending.totalSpent = 0`.

#### `emergencyWithdraw()`
- **Modifiers:** `onlyOwner`, `nonReentrant`
- Full drain of vault balance to owner address.

#### `_resetWindows()` internal
- Resets hourly/daily accumulators if their respective reset timestamps have passed.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `VaultCreated` | Constructor | `owner`, `maxDaily`, `maxPerTx` |
| `TransactionExecuted` | Before each `execute()` call | `to`, `value`, `data` |
| `PolicyUpdated` | `updatePolicy()` | `maxPerTx`, `maxDaily`, `maxHourly` |
| `ContractAllowed` | `allowContract()` on new entry | `target` |
| `ContractRemoved` | `removeContract()` on existing entry | `target` |
| `EmergencyFreeze` | Auto-threshold exceeded or manual `freeze()` | `totalSpent`, `threshold` |
| `EmergencyUnfreeze` | `unfreeze()` | `owner` |
| `FundsDeposited` | `receive()` or `deposit()` | `from`, `amount` |
| `FundsWithdrawn` | `emergencyWithdraw()` | `to`, `amount` |

### Security Invariants

1. **Policy cannot be bypassed by injection.** All checks run on-chain in `execute()`. There is no off-chain component that can whitelist a call.
2. **Reentrancy guard.** `nonReentrant` on `execute()` and `emergencyWithdraw()` prevents the classic reentrancy attack. The `ReentrancyAttacker` mock (see §17) verifies this at test time.
3. **Freeze is permanent until explicit unfreeze.** Once `policy.frozen = true` (either auto or manual), every subsequent `execute()` call reverts with `VaultFrozen()`. Only `unfreeze()` by the owner clears it.
4. **`totalSpent` is never decremented.** Only `unfreeze()` resets it to zero; this prevents an attacker from cycling spend to stay under the emergency threshold.
5. **No rescue for trapped ERC-20 tokens.** The vault handles only native token (ETH/MATIC/etc.). ERC-20 tokens sent to the vault cannot be recovered without a custom `execute()` call.
6. **Owner is immutable.** There is no `transferOwnership()` on `KeyVault`. If the owner key is compromised, the vault cannot be reassigned — only drained via `emergencyWithdraw()`.

---

## 5. VKPFactory.sol

**File:** `vaultfire-keyprotocol/contracts/VKPFactory.sol`
**Role:** VKPFactory — single-call deployer of `KeyVault` instances with per-agent discoverability.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./KeyVault.sol";
     5
     6  /// @title VKPFactory — Deploy KeyVaults with one call
     7  /// @author Vaultfire Protocol
     8  /// @notice Factory for creating KeyVault instances. Tracks all vaults created
     9  ///         per agent for discoverability.
    10  contract VKPFactory {
    11
    12      address public immutable identityRegistry;
    13      uint256 public vaultCount;
    14
    15      mapping(address => address[]) public agentVaults;
    16      address[] public allVaults;
    17
    18      event VaultDeployed(
    19          address indexed owner,
    20          address indexed vault,
    21          uint256 maxDaily,
    22          uint256 maxPerTx
    23      );
    24
    25      error ZeroAddress();
    26
    27      constructor(address _identityRegistry) {
    28          if (_identityRegistry == address(0)) revert ZeroAddress();
    29          identityRegistry = _identityRegistry;
    30      }
    31
    32      function createVault(
    33          uint256 maxPerTx,
    34          uint256 maxHourly,
    35          uint256 maxDaily,
    36          uint256 emergencyThreshold
    37      ) external returns (address vault) {
    38          KeyVault v = new KeyVault(
    39              msg.sender,
    40              identityRegistry,
    40              maxPerTx,
    41              maxHourly,
    42              maxDaily,
    43              emergencyThreshold
    44          );
    45
    46          vault = address(v);
    47          agentVaults[msg.sender].push(vault);
    48          allVaults.push(vault);
    49          vaultCount++;
    50
    51          emit VaultDeployed(msg.sender, vault, maxDaily, maxPerTx);
    52      }
    53
    54      function getAgentVaults(address agent) external view returns (address[] memory) {
    55          return agentVaults[agent];
    56      }
    57
    58      function totalVaults() external view returns (uint256) {
    59          return vaultCount;
    60      }
    61  }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 12 | `identityRegistry` is `immutable`. Factory is permanently bound to one registry — no switching. |
| 15–16 | `agentVaults` maps agent → list of vault addresses. `allVaults` is a global flat list. Both are append-only by contract logic. |
| 32–52 | **`createVault()`** — the core factory function. Deploys a new `KeyVault` via `new KeyVault(msg.sender, ...)`. The `KeyVault` constructor will itself call `identityRegistry.getAgent(msg.sender)` — so if `msg.sender` is not an ERC-8004 registered agent, the `new` call reverts and the factory transaction fails. |
| 38–44 | Parameter order passed to `KeyVault` constructor: `owner=msg.sender`, `identityRegistry`, `maxPerTx`, `maxHourly`, `maxDaily`, `emergencyThreshold`. |
| 47–49 | After successful deployment, both `agentVaults` and `allVaults` are updated, and `vaultCount` is incremented. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `address immutable` | ERC-8004 registry passed to each new `KeyVault` | Immutable |
| `vaultCount` | `uint256` | Total vaults deployed through this factory | Append-only |
| `agentVaults` | `mapping(address => address[])` | Per-agent vault address list | Append-only |
| `allVaults` | `address[]` | Global vault registry | Append-only |

### Functions

#### `createVault(uint256, uint256, uint256, uint256) → address vault`
- No access control — anyone can call, but the `KeyVault` constructor enforces ERC-8004 registration.
- Returns the deployed vault address.

#### `getAgentVaults(address agent) → address[]`
- View — returns all vaults created by a given agent.

#### `totalVaults() → uint256`
- View — global vault count.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `VaultDeployed` | Each successful `createVault()` | `owner`, `vault`, `maxDaily`, `maxPerTx` |

### Security Invariants

- The factory does not own the vaults — each `KeyVault` is owned by `msg.sender` (the agent). The factory only keeps a registry of addresses.
- An agent can deploy multiple vaults (e.g., one per task domain) — the `agentVaults` mapping supports arrays.
- No vault can be unregistered from the factory — addresses are append-only. Historical vault addresses are always discoverable.

---

## 6. SessionKeyManager.sol

**File:** `vaultfire-keyprotocol/contracts/SessionKeyManager.sol`
**Role:** SessionKeyManager — ephemeral, scoped signing keys for AI agents acting on behalf of users.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5
     6  /// @title SessionKeyManager — Time-limited, scoped signing keys for AI agents
     7  contract SessionKeyManager {
     8
     9      struct SessionKey {
    10          address owner;             // ERC-8004 registered agent
    11          address sessionAddress;    // The address authorized to sign
    12          uint256 maxValue;          // Max native token per transaction (wei)
    13          uint256 totalLimit;        // Max total spend over session lifetime (wei)
    14          uint256 totalSpent;        // Running total spent
    15          uint256 expiresAt;         // Unix timestamp when session expires
    16          uint256 createdAt;         // Unix timestamp of creation
    17          bool revoked;              // Manually revoked by owner
    18          bool allowAnyContract;     // If true, can call any contract
    19      }
    20
    21      IVaultfireIdentity public immutable identityRegistry;
    22
    23      uint256 public sessionCount;
    24
    25      mapping(uint256 => SessionKey) public sessions;
    26      mapping(uint256 => mapping(address => bool)) public sessionAllowedContracts;
    27      mapping(address => uint256[]) public ownerSessions;
    28      mapping(address => uint256) public sessionToId;    // reverse lookup
    29
    30      event SessionCreated(uint256 indexed sessionId, address indexed owner,
    31          address indexed sessionAddress, uint256 expiresAt, uint256 maxValue,
    32          uint256 totalLimit);
    33      event SessionUsed(uint256 indexed sessionId, address indexed target, uint256 value);
    34      event SessionRevoked(uint256 indexed sessionId, address indexed owner);
    35      event AllSessionsRevoked(address indexed owner);
    36
    37      error NotRegistered();
    38      error NotSessionOwner();
    39      error SessionExpired();
    40      error SessionRevoked_();
    41      error ExceedsSessionValueLimit(uint256 requested, uint256 limit);
    42      error ExceedsSessionTotalLimit(uint256 wouldSpend, uint256 limit);
    43      error ContractNotAllowedInSession(address target);
    44      error InvalidSessionAddress();
    45      error InvalidDuration();
    46      error ZeroAddress();
    47
    48      constructor(address _identityRegistry) {
    49          if (_identityRegistry == address(0)) revert ZeroAddress();
    50          identityRegistry = IVaultfireIdentity(_identityRegistry);
    51      }
    52
    53      function createSession(
    54          address sessionAddress,
    55          uint256 maxValue,
    56          uint256 totalLimit,
    57          uint256 durationSeconds,
    58          address[] calldata allowedContracts
    59      ) external returns (uint256 sessionId) {
    60          (string memory agentURI,,,) = identityRegistry.getAgent(msg.sender);
    61          if (bytes(agentURI).length == 0) revert NotRegistered();
    62          if (sessionAddress == address(0)) revert InvalidSessionAddress();
    63          if (durationSeconds == 0) revert InvalidDuration();
    64
    65          sessionId = sessionCount++;
    66          uint256 expiresAt = block.timestamp + durationSeconds;
    67
    68          sessions[sessionId] = SessionKey({
    69              owner: msg.sender,
    70              sessionAddress: sessionAddress,
    71              maxValue: maxValue,
    72              totalLimit: totalLimit,
    73              totalSpent: 0,
    74              expiresAt: expiresAt,
    70              createdAt: block.timestamp,
    71              revoked: false,
    72              allowAnyContract: allowedContracts.length == 0
    73          });
    74
    75          for (uint256 i = 0; i < allowedContracts.length; i++) {
    76              sessionAllowedContracts[sessionId][allowedContracts[i]] = true;
    77          }
    78
    79          ownerSessions[msg.sender].push(sessionId);
    80          sessionToId[sessionAddress] = sessionId;
    81
    82          emit SessionCreated(sessionId, msg.sender, sessionAddress, expiresAt, maxValue, totalLimit);
    83      }
    84
    85      function validateSession(uint256 sessionId, address target, uint256 value)
    86          external returns (bool valid) {
    87          SessionKey storage session = sessions[sessionId];
    88          if (block.timestamp > session.expiresAt) revert SessionExpired();
    89          if (session.revoked) revert SessionRevoked_();
    90          if (value > session.maxValue) revert ExceedsSessionValueLimit(value, session.maxValue);
    91          if (session.totalSpent + value > session.totalLimit)
    92              revert ExceedsSessionTotalLimit(session.totalSpent + value, session.totalLimit);
    93          if (!session.allowAnyContract && !sessionAllowedContracts[sessionId][target])
    94              revert ContractNotAllowedInSession(target);
    95          session.totalSpent += value;
    96          emit SessionUsed(sessionId, target, value);
    97          return true;
    98      }
    99
    100     function revokeSession(uint256 sessionId) external {
    101         if (sessions[sessionId].owner != msg.sender) revert NotSessionOwner();
    102         sessions[sessionId].revoked = true;
    103         emit SessionRevoked(sessionId, msg.sender);
    104     }
    105
    106     function revokeAllSessions() external {
    107         uint256[] storage ids = ownerSessions[msg.sender];
    108         for (uint256 i = 0; i < ids.length; i++) {
    109             if (!sessions[ids[i]].revoked) {
    110                 sessions[ids[i]].revoked = true;
    111                 emit SessionRevoked(ids[i], msg.sender);
    112             }
    113         }
    114         emit AllSessionsRevoked(msg.sender);
    115     }
    116
    117     function isSessionValid(uint256 sessionId) external view returns (bool) {
    118         SessionKey storage session = sessions[sessionId];
    119         return !session.revoked && block.timestamp <= session.expiresAt
    120                && session.totalSpent < session.totalLimit;
    121     }
    122
    123     function sessionRemaining(uint256 sessionId) external view returns (uint256) {
    124         SessionKey storage session = sessions[sessionId];
    125         if (session.revoked || block.timestamp > session.expiresAt) return 0;
    126         if (session.totalSpent >= session.totalLimit) return 0;
    127         return session.totalLimit - session.totalSpent;
    128     }
    129
    130     function getOwnerSessions(address owner_) external view returns (uint256[] memory) {
    131         return ownerSessions[owner_];
    132     }
    133
    134     function getSession(uint256 sessionId) external view returns (SessionKey memory) {
    135         return sessions[sessionId];
    136     }
    137
    138     function isContractAllowed(uint256 sessionId, address target) external view returns (bool) {
    139         if (sessions[sessionId].allowAnyContract) return true;
    140         return sessionAllowedContracts[sessionId][target];
    141     }
    142 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 9–19 | **`SessionKey` struct** — 9 fields. `allowAnyContract` is derived from `allowedContracts.length == 0` at creation time; it is a stored boolean, not a live computation. |
| 26 | `sessionAllowedContracts[sessionId][address]` — two-dimensional mapping for per-session contract allowlists. |
| 28 | `sessionToId[sessionAddress]` — reverse lookup from a session signing address back to its session ID. Overwrites if the same address is reused across sessions. |
| 53–83 | **`createSession()`** — checks ERC-8004 registration, allocates session ID from `sessionCount++`, computes `expiresAt = block.timestamp + durationSeconds`. If `allowedContracts` is empty, `allowAnyContract = true`. Populates allowlist mapping. |
| 65 | `sessionId = sessionCount++` — post-increment: first session has ID 0. |
| 80 | `sessionToId[sessionAddress] = sessionId` — overwrites any prior mapping for this address. If the same `sessionAddress` is reused across two sessions, only the latest session ID is in the reverse map. |
| 85–98 | **`validateSession()`** — state-mutating view: updates `totalSpent`. Callers must trust this to be called exactly once per transaction. The caller (e.g., `KeyVault`) is responsible for ensuring the session address is the actual `msg.sender` of the operation — this contract does not check `msg.sender == session.sessionAddress`. |
| 100–104 | **`revokeSession()`** — single-session revocation. Only the session owner can revoke. |
| 106–115 | **`revokeAllSessions()`** — emergency mass revocation. Iterates all session IDs for `msg.sender`. Skips already-revoked sessions. |
| 117–121 | **`isSessionValid()`** — view-only check. Note: the condition `totalSpent < totalLimit` (strict less-than) — a session where `totalSpent == totalLimit` returns `false` from this function but `validateSession()` would also revert because `totalSpent + value > totalLimit` for any positive value. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | ERC-8004 registration check | Immutable |
| `sessionCount` | `uint256` | Session ID counter (auto-increment) | Append-only |
| `sessions` | `mapping(uint256 => SessionKey)` | Session data by ID | Mutable on validate/revoke |
| `sessionAllowedContracts` | `mapping(uint256 => mapping(address => bool))` | Per-session contract allowlist | Set at creation |
| `ownerSessions` | `mapping(address => uint256[])` | Owner → session ID list | Append-only |
| `sessionToId` | `mapping(address => uint256)` | Session address → session ID reverse lookup | Overwritten on new session |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `SessionCreated` | Each `createSession()` | `sessionId`, `owner`, `sessionAddress`, `expiresAt`, `maxValue`, `totalLimit` |
| `SessionUsed` | Each successful `validateSession()` | `sessionId`, `target`, `value` |
| `SessionRevoked` | Single revocation | `sessionId`, `owner` |
| `AllSessionsRevoked` | Mass revocation | `owner` |

### Security Invariants

1. **Session keys expire automatically.** `validateSession()` checks `block.timestamp > expiresAt` and reverts. No action is required to expire a session.
2. **Total lifetime spend is a hard cap.** `totalLimit` is checked against the running `totalSpent`; no window reset resets this.
3. **Revocation is irreversible.** Once `revoked = true`, there is no `unrevokeSession()` function.
4. **`validateSession()` is not access-controlled.** Any caller can invoke it with a valid `sessionId`, `target`, and `value`. If called with `value = 0`, it would consume a transaction count and emit a `SessionUsed` event without checking the caller is actually the session key holder. The caller contract (typically `KeyVault`) must verify caller identity before calling `validateSession()`.
5. **Replay protection.** The session lifetime `totalLimit` prevents indefinite replay. The `revoked` flag enables instant revocation. Expired sessions revert on-chain.

---

## 7. ScopedDelegation.sol

**File:** `vaultfire-keyprotocol/contracts/ScopedDelegation.sol`
**Role:** ScopedDelegation — ERC-7710-compatible scoped permission delegation with progressive narrowing.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5  import "./IVaultfireBonds.sol";
     6  import "./IVaultfireReputation.sol";
     7
     8  /// @title ScopedDelegation — ERC-7710-compatible scoped permissions for AI agent delegation
     9  contract ScopedDelegation {
    10
    11     struct Scope {
    12         uint256 maxValuePerTx;
    13         uint256 maxTotalValue;
    14         uint256 totalSpent;
    15         uint256 maxTxCount;          // 0 = unlimited
    16         uint256 txCount;
    17         uint256 rateLimit;           // Min seconds between transactions (0 = no limit)
    18         uint256 lastTxAt;
    19         uint256 expiresAt;
    20         uint256 createdAt;
    21         address grantor;
    22         address grantee;
    23         bool revoked;
    24         bool active;
    25     }
    26
    27     struct DelegationChain {
    28         uint256[] scopeIds;
    28         uint256 depth;
    29         address rootGrantor;
    30         bool valid;
    31     }
    32
    33     IVaultfireIdentity public immutable identityRegistry;
    34     IVaultfireBonds public immutable bondsRegistry;
    35     IVaultfireReputation public immutable reputationRegistry;
    36
    37     address public owner;
    38
    38     uint256 public totalScopes;
    39     uint256 public totalChains;
    40     uint256 public constant MAX_CHAIN_DEPTH = 5;
    41
    42     mapping(uint256 => Scope) public scopes;
    43     mapping(uint256 => DelegationChain) public chains;
    44     mapping(address => uint256[]) public grantedScopes;
    45     mapping(address => uint256[]) public receivedScopes;
    46     mapping(uint256 => mapping(address => bool)) public contractAllowlist;
    47     mapping(uint256 => mapping(bytes4 => bool)) public functionAllowlist;
    48     mapping(uint256 => bool) public hasContractRestrictions;
    49     mapping(uint256 => bool) public hasFunctionRestrictions;
    50
    51     event ScopeCreated(uint256 indexed scopeId, address indexed grantor,
    52         address indexed grantee, uint256 expiresAt);
    53     event ScopeRevoked(uint256 indexed scopeId, address indexed revokedBy);
    54     event ScopeUsed(uint256 indexed scopeId, address indexed grantee, uint256 value);
    55     event ChainCreated(uint256 indexed chainId, address indexed rootGrantor, uint256 depth);
    56     event ContractAllowlistUpdated(uint256 indexed scopeId, address indexed contractAddr, bool allowed);
    57     event FunctionAllowlistUpdated(uint256 indexed scopeId, bytes4 indexed selector, bool allowed);
    58
    59     modifier onlyOwner() { ... }
    60
    60     modifier onlyRegisteredAgent(address agent) {
    61         (string memory uri, bool active, , ) = identityRegistry.getAgent(agent);
    62         require(bytes(uri).length > 0 && active, "...");
    63         _;
    64     }
    65
    65     constructor(address _identityRegistry, address _bondsRegistry, address _reputationRegistry) { ... }
    66
    67     function createScope(address grantee, uint256 maxValuePerTx, uint256 maxTotalValue,
    68         uint256 maxTxCount, uint256 rateLimit, uint256 duration)
    69         external onlyRegisteredAgent(msg.sender) onlyRegisteredAgent(grantee)
    70         returns (uint256 scopeId) { ... }
    71
    72     function subDelegate(uint256 parentScopeId, address grantee, uint256 maxValuePerTx,
    73         uint256 maxTotalValue, uint256 maxTxCount, uint256 rateLimit, uint256 duration)
    74         external onlyRegisteredAgent(grantee)
    75         returns (uint256 scopeId) { ... }
    76
    77     function setContractAllowlist(uint256 scopeId, address contractAddr, bool allowed) external { ... }
    78     function setFunctionAllowlist(uint256 scopeId, bytes4 selector, bool allowed) external { ... }
    79
    80     function useScope(uint256 scopeId, uint256 value, address targetContract,
    81         bytes4 functionSelector) external returns (bool) { ... }
    82
    82     function revokeScope(uint256 scopeId) external { ... }
    83
    84     // view functions
    84     function isScopeValid(uint256 scopeId) external view returns (bool) { ... }
    85     function getScopeRemaining(uint256 scopeId) external view returns (...) { ... }
    86     function getGrantedScopes(address agent) external view returns (uint256[] memory) { ... }
    87     function getReceivedScopes(address agent) external view returns (uint256[] memory) { ... }
    88     function transferOwnership(address newOwner) external onlyOwner { ... }
    89 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 11–25 | **`Scope` struct** — 14 fields. `rateLimit` is the minimum seconds between consecutive transactions; `lastTxAt` tracks the last transaction timestamp for rate-limit enforcement. `active` and `revoked` are separate flags: `active = false && revoked = false` would represent a scope that was never activated, but in practice all newly created scopes start as `active = true, revoked = false`. |
| 27–31 | **`DelegationChain` struct** — contains an array of scope IDs from root to leaf. The `chains` mapping is defined but no creation function is implemented in this contract (the `ChainCreated` event exists but is never emitted). This struct is infrastructure for future chain tracking. |
| 40 | `MAX_CHAIN_DEPTH = 5` — constant preventing unbounded delegation chains. Not enforced in `subDelegate()` directly (depth tracking would require walking the chain), but signals design intent. |
| 46–49 | Per-scope allowlist mappings. `hasContractRestrictions[scopeId]` is a fast gate: if false, no contract check runs. Same pattern for functions. |
| 60–64 | `onlyRegisteredAgent(agent)` modifier checks both `agentURI.length > 0` **and** `active == true` — stricter than `KeyVault` which only checks URI length. |
| 67–70 | **`createScope()`** — both `msg.sender` (grantor) and `grantee` must be registered active agents. Cannot self-delegate. Duration bounded to `[0, 365 days]`. Must have at least `maxValuePerTx > 0` or `maxTxCount > 0`. |
| 131–166 | Full `createScope()` implementation: allocates `scopeId = totalScopes++`, populates scope struct with `expiresAt = block.timestamp + duration`, pushes to both `grantedScopes[grantor]` and `receivedScopes[grantee]`. |
| 177–227 | **`subDelegate()`** — the progressive narrowing enforcement engine. The caller must be the `grantee` of the parent scope. Five narrowing checks: (1) `maxValuePerTx <= parent.maxValuePerTx`, (2) `maxTotalValue <= parentRemaining` (remaining = `maxTotalValue - totalSpent`), (3) if parent has a tx count limit, new `maxTxCount <= parentRemainingTx`, (4) `rateLimit >= parent.rateLimit` (more restrictive = larger minimum gap), (5) `duration <= parentRemainingDuration`. Any violation reverts. |
| 192–202 | The narrowing invariants are strict checks — `require(maxValuePerTx <= parent.maxValuePerTx, ...)`. Note this allows equality (sub-delegation at the exact same limit as the parent), not just strictly less. |
| 234–243 | **`setContractAllowlist()`** — grantor-only. Sets `hasContractRestrictions[scopeId] = true` when any entry is added. There is no mechanism to unset `hasContractRestrictions` even if all entries are removed — once restrictions are added, they remain checked. |
| 266–308 | **`useScope()`** — stateful validation. Only callable by the scope's `grantee`. Checks: active+not-revoked, not expired, value limit, total value limit, tx count (if set), rate limit, contract allowlist (if set), function allowlist (if set). Then updates `totalSpent`, `txCount`, `lastTxAt`. |
| 315–323 | **`revokeScope()`** — sets both `revoked = true` and `active = false`. Either flag alone would block `useScope()`; setting both is redundant but unambiguous. Can be called by grantor or contract owner. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration check | Immutable |
| `bondsRegistry` | `IVaultfireBonds immutable` | Bond check (present in constructor, not used in scope logic directly) | Immutable |
| `reputationRegistry` | `IVaultfireReputation immutable` | Reputation check (present, not used in scope logic directly) | Immutable |
| `owner` | `address` | Contract owner — can revoke any scope | Transferable |
| `totalScopes` | `uint256` | Scope ID counter | Monotone increasing |
| `scopes` | `mapping(uint256 => Scope)` | All scopes by ID | Mutable |
| `contractAllowlist` | `mapping(uint256 => mapping(address => bool))` | Per-scope contract allowlist | Mutable (grantor) |
| `functionAllowlist` | `mapping(uint256 => mapping(bytes4 => bool))` | Per-scope function selector allowlist | Mutable (grantor) |
| `hasContractRestrictions` | `mapping(uint256 => bool)` | Fast gate for contract checks | Set on first addition, never cleared |
| `hasFunctionRestrictions` | `mapping(uint256 => bool)` | Fast gate for function selector checks | Set on first addition, never cleared |

### Math: Scoped Delegation Expiry

For a root scope created at time T₀ with duration D:

\[ expiresAt = T_0 + D \]

For a sub-delegation from that scope created at time T₁ with requested duration d:

\[ \text{allowed} \iff d \leq expiresAt - T_1 = (T_0 + D) - T_1 \]

Example: Root scope created at T₀=0 with D=30 days (expiresAt=30d). Sub-delegation attempted at T₁=20 days: maximum allowed duration = 30d − 20d = 10 days.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `ScopeCreated` | `createScope()` and `subDelegate()` | `scopeId`, `grantor`, `grantee`, `expiresAt` |
| `ScopeRevoked` | `revokeScope()` | `scopeId`, `revokedBy` |
| `ScopeUsed` | `useScope()` | `scopeId`, `grantee`, `value` |
| `ChainCreated` | (never emitted — infrastructure) | `chainId`, `rootGrantor`, `depth` |
| `ContractAllowlistUpdated` | `setContractAllowlist()` | `scopeId`, `contractAddr`, `allowed` |
| `FunctionAllowlistUpdated` | `setFunctionAllowlist()` | `scopeId`, `selector`, `allowed` |

### Security Invariants

1. **Progressive narrowing is enforced at `subDelegate()` time.** Sub-scopes can never exceed parent limits on value, tx count, or duration. Rate limits can only increase.
2. **Grantee cannot self-delegate.** `require(grantee != msg.sender)`.
3. **Scope usage is caller-gated.** `useScope()` requires `scope.grantee == msg.sender`. An address that is not the grantee cannot record usage against a scope.
4. **Rate limiting prevents rapid-fire abuse.** `rateLimit` sets minimum time between transactions within a scope.
5. **Contract and function restriction flags are sticky.** Once `hasContractRestrictions` is set, it cannot be unset — this prevents a grantor from silently removing restrictions without revoking the scope.

---

## 8. DelegationRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/DelegationRegistry.sol`
**Role:** DelegationRegistry — encrypted key delegation between verified AI agents with live trust reverification.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5  import "./IVaultfireBonds.sol";
     6  import "./IVaultfireReputation.sol";
     7
     8  /// @title DelegationRegistry — Encrypted key delegation between verified AI agents
     9  contract DelegationRegistry {
    10
    11     struct Delegation {
    12         address delegator;
    13         address delegate;
    14         uint256 maxValue;
    15         uint256 totalLimit;
    16         uint256 totalSpent;
    17         uint256 expiresAt;
    18         uint256 createdAt;
    19         uint256 minBondRequired;
    20         uint256 minReputation;
    21         bool revoked;
    22         bool accepted;
    23         bool hasContractAllowlist;
    24         bool hasFunctionAllowlist;
    25         bytes32 encryptedKeyHash;    // Off-chain encrypted session key reference
    26     }
    27
    28     IVaultfireIdentity public immutable identityRegistry;
    29     IVaultfireBonds public immutable bondsRegistry;
    30     IVaultfireReputation public immutable reputationRegistry;
    31
    32     uint256 public delegationCount;
    33
    34     mapping(uint256 => Delegation) public delegations;
    35     mapping(uint256 => mapping(address => bool)) public delegationAllowedContracts;
    36     mapping(uint256 => mapping(bytes4 => bool)) public delegationAllowedFunctions;
    37     mapping(address => uint256[]) public delegatorDelegations;
    38     mapping(address => uint256[]) public delegateDelegations;
    39     mapping(uint256 => uint256) public delegationBondIds;    // bondId used at acceptance
    40
    41     event DelegationCreated(...);
    42     event DelegationAccepted(uint256 indexed delegationId, address indexed delegate);
    43     event DelegationUsed(uint256 indexed delegationId, address indexed target, uint256 value);
    44     event DelegationRevoked(uint256 indexed delegationId, address indexed delegator);
    45     event AllDelegationsRevoked(address indexed delegator);
    46     event TrustViolation(uint256 indexed delegationId, string reason);
    47
    48     // ... errors ...
    49
    50     constructor(address _identityRegistry, address _bondsRegistry, address _reputationRegistry) { ... }
    51
    52     function createDelegation(address delegate, uint256 maxValue, uint256 totalLimit,
    53         uint256 durationSeconds, uint256 minBondRequired, uint256 minReputation,
    54         address[] calldata allowedContracts, bytes4[] calldata allowedFunctions,
    55         bytes32 encryptedKeyHash) external returns (uint256 delegationId) { ... }
    56
    57     function acceptDelegation(uint256 delegationId, uint256 bondId) external { ... }
    58
    58     function verifyTrust(uint256 delegationId) external returns (bool trusted) { ... }
    59
    59     function validateDelegation(uint256 delegationId, address target, uint256 value,
    60         bytes4 selector) external returns (bool valid) { ... }
    61
    62     function revokeDelegation(uint256 delegationId) external { ... }
    63     function revokeAllDelegations() external { ... }
    64
    65     // view functions + _verifyTrustRequirements internal
    66 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 11–26 | **`Delegation` struct** — 14 fields. `encryptedKeyHash` is a `bytes32` reference to an off-chain XMTP/IPFS encrypted session key. This is the "XMTP for keys" aspect: the actual key material is off-chain encrypted; the hash is the on-chain anchor. |
| 19–20 | `minBondRequired` and `minReputation` — the trust gates. Both can be zero (open delegation) or non-zero (trust-gated). |
| 21–22 | `revoked` and `accepted` are separate flags. A delegation that has never been accepted is not usable (`validateDelegation()` reverts with `DelegationNotAccepted`). |
| 34 | `delegationBondIds[delegationId]` — stores the `bondId` the delegate presented at acceptance for future re-verification. |
| 140–202 | **`createDelegation()`** — both `msg.sender` (delegator) and `delegate` must be ERC-8004 registered. Cannot self-delegate. `durationSeconds == 0` reverts. Sets `accepted = false`; the delegation is pending until `acceptDelegation()` is called. |
| 211–241 | **`acceptDelegation(uint256 delegationId, uint256 bondId)`** — delegate calls this to accept the delegation. Validates: not already accepted, not revoked, not expired. If `minBondRequired > 0`: calls `bondsRegistry.getBond(bondId)`, verifies `creator == msg.sender && active && stakeAmount >= minBondRequired`. Stores `bondId` in `delegationBondIds` for future reverification. If `minReputation > 0`: calls `reputationRegistry.getReputation(msg.sender)`, verifies `score >= minReputation`. |
| 253–261 | **`verifyTrust(uint256 delegationId)`** — permissionless re-verification. Returns `false` immediately if already revoked, not accepted, or expired. Otherwise calls `_verifyTrustRequirements()` which may auto-revoke. Returns `!d.revoked` after the check. |
| 273–315 | **`validateDelegation()`** — the execution gate. Checks revoked, accepted, expiry, per-tx value, total limit, contract allowlist, function selector allowlist. Updates `totalSpent`. |
| 322–338 | **`revokeDelegation()`** and **`revokeAllDelegations()`** — immediate revocation by delegator. Mass revocation iterates the full `delegatorDelegations[msg.sender]` array. |
| 385–423 | **`_verifyTrustRequirements()`** — **critical design decision**: this function does NOT revert on trust failure. It calls `bondsRegistry.getBond(...)` and `reputationRegistry.getReputation(...)` in `try/catch` blocks. If the bond or reputation check fails, it sets `d.revoked = true` and returns. If it reverted here, the EVM would roll back the state change and the delegation would remain active — a fail-open security hole. By not reverting, the revocation is committed to chain state even on failure. |
| 391–404 | Bond check in `_verifyTrustRequirements()`: `try bondsRegistry.getBond(bondId) returns (address creator, uint256 stakeAmount, uint256, bool active)`. Verifies `creator == d.delegate && active && stakeAmount >= d.minBondRequired`. If any condition fails, or if the call reverts (the `catch` block), the delegation is auto-revoked with a `TrustViolation` event. |
| 408–422 | Reputation check: `try reputationRegistry.getReputation(d.delegate) returns (uint256 score, ...)`. If `score < d.minReputation`, auto-revoke. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration gate | Immutable |
| `bondsRegistry` | `IVaultfireBonds immutable` | Bond verification | Immutable |
| `reputationRegistry` | `IVaultfireReputation immutable` | Reputation verification | Immutable |
| `delegationCount` | `uint256` | Delegation ID counter | Monotone increasing |
| `delegations` | `mapping(uint256 => Delegation)` | All delegations by ID | Mutable |
| `delegationAllowedContracts` | `mapping(uint256 => mapping(address => bool))` | Per-delegation contract allowlist | Set at creation |
| `delegationAllowedFunctions` | `mapping(uint256 => mapping(bytes4 => bool))` | Per-delegation function selector allowlist | Set at creation |
| `delegatorDelegations` | `mapping(address => uint256[])` | Delegator → delegation ID list | Append-only |
| `delegateDelegations` | `mapping(address => uint256[])` | Delegate → delegation ID list | Append-only |
| `delegationBondIds` | `mapping(uint256 => uint256)` | Delegation → bond ID used at acceptance | Set on acceptance |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `DelegationCreated` | `createDelegation()` | `delegationId`, `delegator`, `delegate`, `expiresAt`, `maxValue`, `totalLimit`, `minBondRequired`, `minReputation` |
| `DelegationAccepted` | `acceptDelegation()` | `delegationId`, `delegate` |
| `DelegationUsed` | `validateDelegation()` | `delegationId`, `target`, `value` |
| `DelegationRevoked` | Manual revocation | `delegationId`, `delegator` |
| `AllDelegationsRevoked` | Mass revocation | `delegator` |
| `TrustViolation` | Auto-revoke in `_verifyTrustRequirements()` | `delegationId`, `reason` string |

### Security Invariants

1. **Two-phase trust gate.** Delegation is created by delegator (pending) and only becomes active when delegate explicitly accepts and proves they meet bond + reputation requirements.
2. **Auto-revocation is fail-closed.** `_verifyTrustRequirements()` uses `try/catch` + commit pattern: any external call failure revokes the delegation rather than keeping it active.
3. **Anyone can trigger reverification.** `verifyTrust()` is permissionless. Watchdog bots can monitor delegation status and call `verifyTrust()` whenever bond or reputation changes.
4. **No delegation rescue.** Once revoked (manually or automatically), a delegation cannot be reinstated. A new delegation must be created and accepted.
5. **`encryptedKeyHash` is a reference, not the key.** Actual key material lives off-chain. The hash provides auditability without key exposure on a public ledger.

---

## 9. AgentSLAEnforcer.sol

**File:** `vaultfire-keyprotocol/contracts/AgentSLAEnforcer.sol`
**Role:** AgentSLAEnforcer — on-chain SLA commitment registry with staked collateral and automatic penalty slashing.

### Full Source with Line Numbers

(Full source: 372 lines — see source files. Key structures documented below.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 19–31 | **`SLACommitment` struct** — three metrics (`uptimeTarget`, `responseTimeMs`, `accuracyTarget`) expressed in basis points. `stakedAmount` is native token locked as SLA guarantee. `penaltyRate` is in basis points of remaining stake per violation. |
| 33–46 | **`SLAReport` struct** — captures a measurement period with individual violation flags per metric. `penaltyApplied` records how much was actually slashed. |
| 63–69 | **Constants**: `MIN_UPTIME_TARGET = 5000` (50% minimum — no SLA below 50% uptime), `MAX_RESPONSE_TIME = 60000` (60 seconds max), `MIN_ACCURACY_TARGET = 5000`, duration bounded to `[7 days, 365 days]`, `MAX_PENALTY_RATE = 1000` (10% maximum per-violation slash), `MIN_REPORT_PERIOD = 1 hour`. |
| 76–77 | `monitors` mapping — authorized addresses that can submit SLA performance reports. The owner is always an authorized monitor. |
| 103–106 | `onlyMonitor` modifier — owner OR any authorized monitor address. |
| 143–177 | **`createSLA()`** — `payable`, requires `msg.value > 0`. All three metrics validated against their constants. SLA stored with `activeSince = block.timestamp` and `expiresAt = block.timestamp + duration`. |
| 190–276 | **`submitReport()`** — monitor-only. Validates: SLA active, not expired, period valid (past, at least 1 hour long). Detects up to 3 violations per report (uptime, response time, accuracy). Penalty calculation detailed below. |
| 207–211 | Violation detection: `uptimeViolation = measuredUptime < sla.uptimeTarget`, `responseViolation = measuredResponseTime > sla.responseTimeMs`, `accuracyViolation = measuredAccuracy < sla.accuracyTarget`. |
| 214–252 | Penalty block: counts violations, computes penalty, updates counters, deactivates SLA if stake fully consumed. |
| 283–299 | **`expireSLA()`** — called after `block.timestamp >= sla.expiresAt`. Sets `active = false`, returns `remainingStake = stakedAmount - totalPenalties` to agent. |

### Math: SLA Penalty

For a report with `n` violations:

\[ \text{remainingStake} = \text{stakedAmount} - \text{totalPenalties} \]

\[ \text{penalty} = \left\lfloor \frac{\text{remainingStake} \times \text{penaltyRate} \times n}{10000} \right\rfloor \]

where `penaltyRate` is in basis points.

**Penalty is capped at `remainingStake`** — the agent's stake can reach zero but cannot go negative.

Each violation type (uptime, response, accuracy) receives `penalty / n` in the emitted event.

**Example:**
- `stakedAmount = 1 ETH`, `totalPenalties = 0.1 ETH`, `penaltyRate = 500` (5%), `n = 2` violations
- `remainingStake = 0.9 ETH`
- `penalty = (0.9e18 × 500 × 2) / 10000 = 0.09 ETH`
- Each of the two `SLAViolation` events logs `0.045 ETH`
- After report: `totalPenalties = 0.19 ETH`, `penaltyPool += 0.09 ETH`

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin (monitor management, penalty withdrawal) | Transferable |
| `totalSLAs / totalReports / totalViolations / totalPenaltiesCollected` | `uint256` | Global counters | Monotone increasing |
| `slas` | `mapping(uint256 => SLACommitment)` | SLA data by ID | Mutable |
| `reports` | `mapping(uint256 => SLAReport)` | Report data by ID | Append-only |
| `monitors` | `mapping(address => bool)` | Authorized report submitters | Mutable (owner) |
| `penaltyPool` | `uint256` | Accumulated penalty balance | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `SLACreated` | `createSLA()` | `slaId`, `agent`, `uptimeTarget`, `responseTimeMs`, `accuracyTarget`, `stake` |
| `SLAExpired` | `expireSLA()` or stake exhaustion | `slaId`, `agent`, `totalViolations` |
| `SLAReportSubmitted` | `submitReport()` | `reportId`, `slaId`, `reporter`, `hasViolation` |
| `SLAViolation` | Per-violation in `submitReport()` | `slaId`, `agent`, `penalty` (per-violation share), `violationType` |
| `SLAStakeReturned` | `expireSLA()` with remaining stake | `slaId`, `agent`, `amount` |
| `MonitorUpdated` | `setMonitor()` | `monitor`, `authorized` |
| `PenaltyWithdrawn` | `withdrawPenalties()` | `to`, `amount` |

### Security Invariants

1. **Staking is required.** `msg.value > 0` enforced at `createSLA()`. An SLA without stake is not permitted.
2. **Penalty cannot exceed remaining stake.** Cap at `remainingStake` prevents arithmetic underflow.
3. **Report period must be in the past.** `require(periodEnd <= block.timestamp)` prevents forward-dated reports.
4. **Monitor access is strictly controlled.** Only owner can add/remove monitors. A compromised monitor can submit false reports — monitor addresses should be hardware-attested or multi-sig controlled.
5. **SLA stake is not slashable externally.** Only `submitReport()` can change `totalPenalties`; only `expireSLA()` returns the remainder. There is no emergency drain function for SLA stakes.

---

## 10. AgentInsurancePool.sol

**File:** `vaultfire-keyprotocol/contracts/AgentInsurancePool.sol`
**Role:** AgentInsurancePool — pooled insurance for AI agents with reputation-tiered premiums and arbiter-resolved claims.

### Full Source with Line Numbers

(Full source: 320 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 19–28 | **`Policy` struct** — `riskTier` is `uint256` (1, 2, or 3). `claimsPaid` accumulates total claims paid against this policy to track per-policy coverage exhaustion. |
| 30–39 | **`Claim` struct** — `evidenceURI` points to external evidence (IPFS). `resolvedAt = 0` when pending. `ClaimStatus` enum is `{Pending, Approved, Denied, Paid}` — note `Approved` and `Paid` are separate states (arbiter approves, then payout executes). |
| 41 | `ClaimStatus { Pending, Approved, Denied, Paid }` — `resolveClaim()` transitions from `Pending` directly to `Paid` (if approved) or `Denied` in a single transaction, skipping `Approved` as an intermediate state. The `Approved` enum value exists for potential future multi-step resolution. |
| 59–62 | **Premium rate constants**: `LOW_RISK_RATE = 100` (1%), `MEDIUM_RISK_RATE = 250` (2.5%), `HIGH_RISK_RATE = 500` (5%). These are basis points of coverage. |
| 64–67 | **Risk tier thresholds**: reputation ≥ 70 → tier 1 (low), ≥ 40 → tier 2 (medium), below 40 → tier 3 (high). |
| 136–169 | **`createPolicy()`** — `onlyRegisteredAgent(msg.sender)`, `payable`. Enforces one active policy per agent (`hasActivePolicy`). Calls `_calculateRiskTier()` which queries the reputation registry. Calls `_calculatePremium()` to get the required premium. If `msg.value < requiredPremium`, reverts. Adds `msg.value` to `totalPoolBalance`. |
| 173–182 | **`expirePolicy()`** — permissionless. Moves policy to inactive and clears `hasActivePolicy`. Does not refund any premium. |
| 192–219 | **`fileClaim()`** — permissionless. Any address can file a claim against an active, non-expired policy. Validates amount ≤ `coverageLimit` and cumulative `claimsPaid + amount ≤ coverageLimit`. |
| 224–249 | **`resolveClaim()`** — arbiter-only. If approved: checks `totalPoolBalance >= amount`, updates `policy.claimsPaid`, decrements `totalPoolBalance`, increments `totalClaimsPaid`, sends ETH to claimant. If denied: sets status to `Denied`. No refund of premium either way. |
| 256–261 | **`_calculateRiskTier()`** — pure reputation read; does not consider bond size or violation history. |
| 264–276 | **`_calculatePremium()`** — `pure` function. Formula below. |

### Math: Insurance Pool

**Risk Tier Assignment:**

\[ \text{tier} = \begin{cases} 1 & \text{if } \text{rep} \geq 70 \\ 2 & \text{if } 40 \leq \text{rep} < 70 \\ 3 & \text{if } \text{rep} < 40 \end{cases} \]

**Premium Calculation:**

\[ \text{premium} = \frac{\text{coverageLimit} \times \text{rate} \times \text{duration}}{365\,\text{days} \times 10000} \]

where `rate` ∈ {100, 250, 500} basis points.

**Examples (1 ETH coverage, 30-day policy):**

| Tier | Rate | Premium |
|------|------|---------|
| 1 (low risk, rep ≥ 70) | 100 bp (1%) | `1e18 × 100 × 30d / (365d × 10000)` ≈ 0.000822 ETH |
| 2 (medium risk, rep 40–69) | 250 bp (2.5%) | ≈ 0.00205 ETH |
| 3 (high risk, rep < 40) | 500 bp (5%) | ≈ 0.00411 ETH |

**Coverage Exhaustion:**

`policy.claimsPaid + newClaim.amount ≤ policy.coverageLimit` — once the coverage limit is fully consumed, no further claims can be filed against that policy.

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin (set arbiter, transfer ownership) | Transferable |
| `arbiter` | `address` | Claim resolver — initially set to `owner` | Mutable (owner) |
| `totalPoolBalance` | `uint256` | Aggregate premium balance available for claims | Mutable |
| `totalPolicies / totalClaims / totalClaimsPaid` | `uint256` | Global statistics | Monotone increasing |
| `policies` | `mapping(uint256 => Policy)` | Policy data by ID | Mutable |
| `claims` | `mapping(uint256 => Claim)` | Claim data by ID | Mutable |
| `hasActivePolicy` | `mapping(address => bool)` | One-policy-per-agent enforcement | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `PolicyCreated` | `createPolicy()` | `policyId`, `agent`, `coverageLimit`, `premium`, `riskTier` |
| `PolicyExpired` | `expirePolicy()` | `policyId`, `agent` |
| `ClaimFiled` | `fileClaim()` | `claimId`, `policyId`, `claimant`, `amount` |
| `ClaimResolved` | `resolveClaim()` | `claimId`, `status`, `payout` |
| `PremiumDeposited` | `createPolicy()` | `agent`, `amount` |
| `ArbiterUpdated` | `setArbiter()` | `oldArbiter`, `newArbiter` |

### Security Invariants

1. **Single active policy per agent.** `hasActivePolicy[agent]` prevents double-coverage abuse.
2. **Arbiter is the single claim resolution authority.** A compromised arbiter can approve fraudulent claims. Multi-sig arbiter address is strongly recommended.
3. **No pool solvency guarantee.** `totalPoolBalance` can theoretically be drained if many large claims are approved simultaneously. No partial-payout mechanism exists.
4. **Premium is locked at policy creation time.** Risk tier is computed once from the reputation registry at `createPolicy()` time, not dynamically repriced. An agent's reputation can drop after purchasing insurance without penalty repricing.
5. **Evidence URI is not validated on-chain.** `evidenceURI` is a string; empty-string check (`bytes(evidenceURI).length > 0`) is the only validation.

---

## 11. AgentSafetyManager.sol

**File:** `vaultfire-keyprotocol/contracts/AgentSafetyManager.sol`
**Role:** AgentSafetyManager — kill switch, safe mode, and EU AI Act Article 36 compliant safety controls.

### Full Source with Line Numbers

(Full source: 320 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 20 | `SafetyStatus { Active, SafeMode, Frozen }` — the three safety states. `Active = 0`, `SafeMode = 1`, `Frozen = 2` in EVM encoding. |
| 22–30 | **`AgentSafetyState` struct** — records the safety state, who triggered it, human-readable reason, and counts of auto vs. manual trigger events. `exists` flag distinguishes "never had a record" (implicitly `Active`) from "explicitly set to Active". |
| 32–40 | **`SafetyEvent` struct** — full audit trail entry with `fromStatus` and `toStatus`. Pushed to `safetyLog[]` on every status change. |
| 53–54 | `safeModeThreshold = 30` and `freezeThreshold = 10` — default thresholds. Configurable by owner, with the invariant `safeModeThreshold > freezeThreshold`. |
| 83–100 | `onlyOwner` and `onlySafetyOperator` modifiers. Safety operators can trigger safety actions but cannot update thresholds or add other operators. |
| 125–133 | **`freezeAgent()`** — kill switch. Callable by owner or any safety operator. Requires agent to be registered. Calls `_updateSafetyStatus()` with `isAutoTrigger = false`. |
| 138–143 | **`activateSafeMode()`** — restricts agent to read-only mode. Same permissions as `freezeAgent()`. |
| 148–157 | **`reactivateAgent()`** — unfreezes or removes safe mode. Requires an existing safety record; cannot "activate" an agent that was never recorded (they are implicitly active). |
| 162–176 | **`checkAndEnforceSafety()`** — permissionless reputation check. Anyone can call. Reads score from `reputationRegistry`. If `score <= freezeThreshold (10)`: auto-freeze (if not already frozen). If `score <= safeModeThreshold (30)`: auto-safe-mode (if currently Active). Note: if agent is already in SafeMode and score drops to freezeThreshold, a second call is needed (the first call puts them in SafeMode; a second call would freeze them since the SafeMode branch is `state.status == SafetyStatus.Active`). |
| 182–235 | **`_updateSafetyStatus()`** — internal. Updates `agentSafety[agent]`, adjusts global counters (`totalFrozenAgents`, `totalSafeModeAgents`), appends `SafetyEvent` to `safetyLog` and `agentSafetyEvents[agent]`. Emits the typed event. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin | Transferable |
| `safeModeThreshold` | `uint256` | Rep below this → auto safe mode (default 30) | Mutable (owner) |
| `freezeThreshold` | `uint256` | Rep below this → auto freeze (default 10) | Mutable (owner) |
| `agentSafety` | `mapping(address => AgentSafetyState)` | Per-agent safety state | Mutable |
| `safetyLog` | `SafetyEvent[]` | Global audit trail | Append-only |
| `agentSafetyEvents` | `mapping(address => uint256[])` | Agent → indices into `safetyLog` | Append-only |
| `safetyOperators` | `mapping(address => bool)` | Authorized safety triggerers | Mutable (owner) |
| `totalFrozenAgents / totalSafeModeAgents` | `uint256` | Global safety counters | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `AgentFrozen` | Freeze via operator or auto | `agent`, `triggeredBy`, `reason` |
| `AgentSafeModeActivated` | Safe mode via operator or auto | `agent`, `triggeredBy`, `reason` |
| `AgentReactivated` | Reactivation | `agent`, `triggeredBy`, `reason` |
| `SafetyThresholdsUpdated` | `setThresholds()` | `safeModeThreshold`, `freezeThreshold` |
| `SafetyOperatorUpdated` | `setSafetyOperator()` | `operator`, `authorized` |

### Security Invariants

1. **Permissionless auto-trigger.** Anyone can call `checkAndEnforceSafety()`. This allows watchdog bots and users to enforce safety without relying on the protocol team.
2. **Safety state changes cannot be triggered by unregistered addresses.** The `onlyRegisteredAgent` modifier guards all safety-setting functions, preventing spurious records for non-existent agents.
3. **Audit trail is append-only.** The `safetyLog` array never deletes or modifies entries. Regulatory auditors can reconstruct every status transition with full context.
4. **Threshold invariant.** `setThresholds()` enforces `safeModeThreshold > freezeThreshold` — the two thresholds cannot overlap or invert.

---

## 12. ComplianceRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/ComplianceRegistry.sol`
**Role:** ComplianceRegistry — EU AI Act on-chain attestation system with authorized auditor management.

### Full Source with Line Numbers

(Full source: 298 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 17 | `RiskLevel { Minimal, Limited, High, Unacceptable }` — EU AI Act Article 5/6/9 risk tiers. |
| 19 | `ComplianceStatus { Pending, Compliant, NonCompliant, Expired, Revoked }` — `Pending` exists as a default enum value but no attestation is ever created with `Pending` status; all issued attestations start as `Compliant`. |
| 21–35 | **`ComplianceAttestation` struct** — 14 fields. `humanOversightVerified`, `technicalDocVerified`, `postMarketMonitoring`, `incidentReporting` map directly to EU AI Act high-risk requirements. |
| 37–42 | **`AuditorInfo` struct** — tracks authorization timestamp and count of attestations issued. `auditorURI` is a link to auditor credentials (e.g., accreditation certificate on IPFS). |
| 56–57 | Constants: `MIN_ATTESTATION_DURATION = 30 days`, `MAX_ATTESTATION_DURATION = 730 days` (2 years). |
| 62 | `isCompliant[agent]` — fast O(1) compliance lookup. This mapping is updated by `_recalculateCompliance()` when attestations expire or are revoked. |
| 119–173 | **`issueAttestation()`** — authorized auditor only. Hard block: `Unacceptable` risk level cannot be attested. For `High` risk: all four boolean flags must be `true`. Creates attestation with `status = Compliant`, updates `isCompliant[agent] = true` if not already. |
| 178–190 | **`revokeAttestation()`** — authorized auditor or owner. Sets `status = Revoked`, records `revokedAt` timestamp. Calls `_recalculateCompliance()` to check if agent still has any valid attestation. |
| 194–203 | **`expireAttestation()`** — permissionless. Any caller can mark an attestation expired once `block.timestamp >= att.expiresAt`. Calls `_recalculateCompliance()`. |
| 210–227 | **`_recalculateCompliance()`** — loops over all attestations for the agent, looking for any with `status == Compliant && block.timestamp < expiresAt`. If none found, sets `isCompliant[agent] = false` and decrements `totalCompliantAgents`. |
| 234–248 | **`checkCompliance()`** — view function computing current compliance from all attestations (does not use the `isCompliant` cache, so returns the live computed value). |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration check | Immutable |
| `owner` | `address` | Admin — auditor management | Transferable |
| `totalAttestations / totalAuditors / totalCompliantAgents` | `uint256` | Global statistics | Mutable |
| `attestations` | `mapping(uint256 => ComplianceAttestation)` | Attestation data | Mutable |
| `agentAttestations` | `mapping(address => uint256[])` | Agent → attestation IDs | Append-only |
| `auditors` | `mapping(address => AuditorInfo)` | Authorized auditor registry | Mutable (owner) |
| `isCompliant` | `mapping(address => bool)` | Fast compliance lookup | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `AttestationIssued` | `issueAttestation()` | `attestationId`, `agent`, `auditor`, `riskLevel`, `framework` |
| `AttestationRevoked` | `revokeAttestation()` | `attestationId`, `agent`, `auditor`, `reason` |
| `AttestationExpired` | `expireAttestation()` | `attestationId`, `agent` |
| `AuditorAuthorized` | `authorizeAuditor()` | `auditor`, `auditorURI` |
| `AuditorRevoked` | `revokeAuditor()` | `auditor` |
| `ComplianceStatusChanged` | Compliance gained or lost | `agent`, `isCompliant` |

### Security Invariants

1. **`Unacceptable` risk agents cannot receive attestations.** Hard-coded `require(riskLevel != RiskLevel.Unacceptable)`.
2. **High-risk agents must satisfy all four EU AI Act checks.** `humanOversight && technicalDoc && postMarket && incident` — no partial compliance for High risk.
3. **Auditor revocation does not automatically invalidate past attestations.** Revoking an auditor via `revokeAuditor()` prevents them from issuing new attestations but does not retroactively revoke existing ones.
4. **`isCompliant` cache can diverge from live state.** `expireAttestation()` must be called manually for the cache to update. `checkCompliance()` (view, not cached) always reflects the true live state.

---

## 13. ReputationDecay.sol

**File:** `vaultfire-keyprotocol/contracts/ReputationDecay.sol`
**Role:** ReputationDecay — time-based reputation decay engine with context-specific scoring and recovery bonuses.

### Full Source with Line Numbers

(Full source: 364 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 18–27 | **`DecayProfile` struct** — `baseScore` from the external registry, `decayedScore` is the live score, `lastActivityAt` is the key decay timestamp. |
| 29–34 | **`ContextReputation` struct** — per-context score (e.g., "trading", "code_review"). Initialized at 50 on first activity. |
| 36–43 | **`ActivityRecord` struct** — full audit entry for each recorded activity. |
| 55–58 | **Decay parameters** (configurable): `decayRatePerDay = 1` (1 point per day), `decayGracePeriod = 7 days` (no decay for first 7 days of inactivity), `minDecayedScore = 5` (floor), `recoveryBonusMax = 10` (max bonus from below-50 recovery). |
| 128–142 | **`initializeProfile()`** — seeds `baseScore` and `decayedScore` from the external reputation registry. Sets `lastActivityAt = block.timestamp`. If profile already exists, updates `baseScore` and `decayedScore` from registry. |
| 153–226 | **`recordActivity()`** — reporter-only. First applies pending decay via `_applyDecay()`. For positive activities: applies `weight` bonus plus optional recovery bonus. For negative activities: applies `weight × 2` penalty. Also updates context-specific score. Appends to `activityLog`. |
| 171–177 | **Recovery bonus logic**: if `decayedScore < 50` and the activity is positive, the recovery bonus is `(50 - decayedScore) × recoveryBonusMax / 50`. At score 0, bonus = `recoveryBonusMax` (max 10). At score 49, bonus ≈ 0.2. |
| 178 | `_min(decayedScore + boost, 100)` — caps score at 100. |
| 180–187 | Negative activity: `penalty = weight × 2`. If `decayedScore > penalty`, subtracts; otherwise clamps to `minDecayedScore`. |
| 238–267 | **`_applyDecay()`** — internal. Computes `timeSinceActivity = block.timestamp - lastActivityAt`. If within grace period: no decay, updates `lastDecayCalculation`. If beyond grace: `decayDays = (timeSinceActivity - gracePeriod) / 1 days`, `totalDecay = decayDays × decayRatePerDay`. Applies decay down to `minDecayedScore`. |
| 274–299 | **`getCurrentScore()`** — view-only computation of what the decayed score would be without mutating state. |

### Math: Reputation Decay Formula

**Full formula:**

\[ S(t) = \max\!\left(\, S_0 - \left\lfloor \frac{\max(0,\; t - t_{\text{last}} - t_{\text{grace}})}{86400} \right\rfloor \times r,\; S_{\text{min}} \right) \]

where:
- \(S_0\) = score at last activity
- \(t\) = current timestamp
- \(t_{\text{last}}\) = `lastActivityAt`
- \(t_{\text{grace}}\) = `decayGracePeriod` (default 7 days = 604,800 seconds)
- \(r\) = `decayRatePerDay` (default 1 point/day)
- \(S_{\text{min}}\) = `minDecayedScore` (default 5)

**Example over time (starting score = 80, defaults):**

| Day | Calculation | Score |
|-----|-------------|-------|
| 0 | Activity recorded | 80 |
| 7 | End of grace period | 80 |
| 8 | 1 day past grace | 80 − 1 = 79 |
| 14 | 7 days past grace | 80 − 7 = 73 |
| 30 | 23 days past grace | 80 − 23 = 57 |
| 75 | 68 days past grace | 80 − 68 = 12 |
| 82 | 75 days past grace | 80 − 75 = 5 (floor) |
| 100 | 93 days past grace | Stays at 5 (floor) |

**Recovery Bonus (score < 50):**

\[ \text{bonus} = \left\lfloor \frac{(50 - S) \times S_{\text{recov}}}{50} \right\rfloor \]

At \(S = 10\), \(S_{\text{recov}} = 10\): bonus = \(\lfloor 40 \times 10 / 50 \rfloor = 8\) points on top of `weight`.

**Context Score (no decay applied per-context):**

Context scores are updated by positive/negative activities with `weight` and `weight × 2` respectively. Context scores are not subject to the global time-decay — only the global `decayedScore` decays.

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin | Transferable |
| `decayRatePerDay` | `uint256` | Points decayed per day of inactivity (1–10) | Mutable (owner) |
| `decayGracePeriod` | `uint256` | Inactivity grace period before decay starts | Mutable (owner) |
| `minDecayedScore` | `uint256` | Floor for decayed score (≤ 20) | Mutable (owner) |
| `recoveryBonusMax` | `uint256` | Max recovery bonus for low-score agents (≤ 20) | Mutable (owner) |
| `profiles` | `mapping(address => DecayProfile)` | Per-agent decay profiles | Mutable |
| `contextScores` | `mapping(address => mapping(bytes32 => ContextReputation))` | Per-agent per-context scores | Mutable |
| `activityLog` | `ActivityRecord[]` | Global activity audit log | Append-only |
| `reporters` | `mapping(address => bool)` | Authorized activity reporters | Mutable (owner) |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `ActivityRecorded` | `recordActivity()` | `agent`, `context`, `positive`, `weight`, `reporter` |
| `ScoreDecayed` | `_applyDecay()` when score changes | `agent`, `oldScore`, `newScore`, `daysSinceActivity` |
| `ProfileCreated` | First `initializeProfile()` | `agent`, `baseScore` |
| `ReporterUpdated` | `setReporter()` | `reporter`, `authorized` |
| `DecayParametersUpdated` | `setDecayParameters()` | `ratePerDay`, `gracePeriod`, `minScore` |

### Security Invariants

1. **Activity reporters are strictly controlled.** Only authorized reporters can call `recordActivity()`. A compromised reporter can inflate or deflate scores.
2. **Decay is lazy.** Scores are not automatically decayed on every block. Decay is applied when `recordActivity()` or `applyDecay()` is called. The `getCurrentScore()` view function shows what the score would be without mutating state.
3. **Profiles must be initialized before recording activities.** `recordActivity()` requires `profile.exists` — `initializeProfile()` must be called first.
4. **Context scores are not decay-eligible.** Only the global `decayedScore` in `DecayProfile` undergoes time decay. Context scores (`ContextReputation.score`) only change through explicit `recordActivity()` calls.
5. **`baseScore` diverges from `decayedScore` over time.** `baseScore` reflects the last value read from the external reputation registry via `initializeProfile()`. `decayedScore` is the live VKP-internal score. Applications must query `getCurrentScore()` for the live value, not `profiles[agent].decayedScore` (which may be stale since the last `_applyDecay()` call).

---

## 14. MockIdentityRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockIdentityRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireIdentity` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireIdentity.sol";
     5
     6  /// @dev Mock identity registry for testing.
     7  contract MockIdentityRegistry is IVaultfireIdentity {
     8      mapping(address => bool) private _registered;
     9      mapping(address => uint256) private _registeredAt;
    10
    11     function register(address agent) external {
    12         _registered[agent] = true;
    13         _registeredAt[agent] = block.timestamp;
    14     }
    15
    16     function unregister(address agent) external {
    17         _registered[agent] = false;
    18     }
    17
    18     function getAgent(address agent) external view override returns (
    19         string memory agentURI, bool active, string memory agentType, uint256 registeredAt)
    20     {
    21         if (_registered[agent]) {
    22             return ('{"name":"mock-agent"}', true, "agent", _registeredAt[agent]);
    23         }
    24         return ("", false, "", 0);
    25     }
    26 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 7 | Inherits `IVaultfireIdentity` — implements the interface exactly, suitable as a drop-in test double. |
| 8–9 | Simple boolean + timestamp storage. No URI storage. |
| 11–14 | `register()` — permissionless in the mock (no access control), records `block.timestamp`. |
| 16–18 | `unregister()` — permissionless, sets `_registered = false`. |
| 18–25 | `getAgent()` returns a hardcoded JSON string `'{"name":"mock-agent"}'` for all registered addresses — sufficient for `bytes(agentURI).length > 0` checks. |

### Security Invariants

- **Test-only.** No access control on `register()` / `unregister()`. Never deploy in production.
- The returned `agentType = "agent"` and `active = true` match the `onlyRegisteredAgent` modifier check in `ScopedDelegation` and `AgentSLAEnforcer` that verify both `bytes(uri).length > 0 && active`.

---

## 15. MockBondsRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockBondsRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireBonds` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireBonds.sol";
     5
     6  contract MockBondsRegistry is IVaultfireBonds {
     7      struct Bond {
     8          address creator;
     9          uint256 stakeAmount;
    10         uint256 createdAt;
    11         bool active;
    12     }
    13
    14     mapping(uint256 => Bond) private _bonds;
    15     uint256 public bondCount;
    16
    17     function createBond(address creator, uint256 stakeAmount) external returns (uint256 bondId) {
    18         bondId = bondCount++;
    19         _bonds[bondId] = Bond({ creator: creator, stakeAmount: stakeAmount,
    20             createdAt: block.timestamp, active: true });
    21     }
    22
    23     function setBondStake(uint256 bondId, uint256 newStake) external {
    24         _bonds[bondId].stakeAmount = newStake;
    25     }
    26
    27     function deactivateBond(uint256 bondId) external { _bonds[bondId].active = false; }
    28     function activateBond(uint256 bondId) external { _bonds[bondId].active = true; }
    29
    30     function getBond(uint256 bondId) external view override returns (
    31         address creator, uint256 stakeAmount, uint256 createdAt, bool active)
    32     {
    33         Bond storage b = _bonds[bondId];
    34         return (b.creator, b.stakeAmount, b.createdAt, b.active);
    35     }
    36 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 17–21 | `createBond()` — allocates from `bondCount++`, starts active. |
| 23–25 | `setBondStake()` — allows tests to simulate stake reduction (testing `verifyTrust()` auto-revoke on bond drop). |
| 27–28 | `deactivateBond()` / `activateBond()` — toggle `active` flag for testing `_verifyTrustRequirements()`. |
| 30–35 | `getBond()` returns `(0, 0, 0, false)` for non-existent bonds — `getBond(999)` returns `creator = address(0)`, which fails the `creator != msg.sender` check in `DelegationRegistry.acceptDelegation()`. |

---

## 16. MockReputationRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockReputationRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireReputation` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireReputation.sol";
     5
     6  contract MockReputationRegistry is IVaultfireReputation {
     7      struct Reputation {
     8          uint256 score;
     9          uint256 totalActions;
    10         uint256 lastUpdated;
    11     }
    12
    13     mapping(address => Reputation) private _reputations;
    14
    15     function setReputation(address agent, uint256 score, uint256 totalActions) external {
    16         _reputations[agent] = Reputation({
    17             score: score, totalActions: totalActions, lastUpdated: block.timestamp
    18         });
    19     }
    20
    21     function getReputation(address agent) external view override returns (
    22         uint256 score, uint256 totalActions, uint256 lastUpdated)
    23     {
    24         Reputation storage r = _reputations[agent];
    25         return (r.score, r.totalActions, r.lastUpdated);
    26     }
    27 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 15–19 | `setReputation()` — permissionless score setter. Allows test setup of arbitrary scores (0–100+) to exercise tier boundaries in `AgentInsurancePool` and threshold enforcement in `AgentSafetyManager`. |
| 21–26 | Returns `(0, 0, 0)` for agents with no set reputation — a zero score means high risk (tier 3) in `AgentInsurancePool` and triggers auto-freeze in `AgentSafetyManager` (since 0 ≤ freezeThreshold=10). |

---

## 17. ReentrancyAttacker.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/ReentrancyAttacker.sol`
**Role:** Mock — adversarial contract proving `KeyVault.execute()` is reentrancy-safe.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../KeyVault.sol";
     5
     6  /// @dev Malicious contract that tries to re-enter KeyVault during execute()
     7  contract ReentrancyAttacker {
     8      KeyVault public target;
     9      uint256 public attackCount;
    10     bool public attacking;
    11
    12     constructor(address _target) {
    13         target = KeyVault(payable(_target));
    14     }
    15
    16     /// @dev When this contract receives ETH, it tries to re-enter execute()
    17     receive() external payable {
    18         if (attacking && attackCount < 3) {
    19             attackCount++;
    20             try target.execute(address(this), 0.01 ether, "") {
    21                 // If this succeeds, the reentrancy guard is broken
    22             } catch {
    23                 // Expected: should revert with "ReentrancyGuard: reentrant call"
    24             }
    25         }
    26     }
    27
    28     function startAttack() external { attacking = true; attackCount = 0; }
    29     function stopAttack() external { attacking = false; }
    30 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 17–26 | `receive()` is triggered when `KeyVault.execute()` sends ETH to this contract. The attacker attempts up to 3 recursive `execute()` calls. Each attempt is wrapped in `try/catch` — the test verifies that all re-entrant calls are caught and reverted by `KeyVault`'s `nonReentrant` modifier. |
| 20 | The re-entrant call passes `value = 0.01 ether` — enough to exceed daily limits in combination with the outer call, so even if the guard failed, spending limits would catch it. The guard fires first. |
| 28–29 | `startAttack()` / `stopAttack()` allow test setup to arm and disarm the attacker. |

### Security Invariants Proven

- The `_ENTERED` flag set before `to.call{value: value}(data)` in `KeyVault.execute()` is still set when `receive()` is triggered in the attacker, causing the nested `execute()` call to revert with `"ReentrancyGuard: reentrant call"`.

---

## Math Reference

### SLA Penalty Math (AgentSLAEnforcer)

```
penalty = (remainingStake × penaltyRate × violationCount) / 10000
where:
  remainingStake  = stakedAmount - totalPenalties (running balance)
  penaltyRate     ∈ (0, 1000] basis points (0% < rate ≤ 10%)
  violationCount  ∈ {1, 2, 3}
  penalty         = min(penalty, remainingStake)  (cap at zero)
```

### Insurance Premium Math (AgentInsurancePool)

```
premium = (coverageLimit × rate × duration) / (365 days × 10000)
where:
  rate = 100 bp (tier 1), 250 bp (tier 2), or 500 bp (tier 3)
  tier determined by getReputation().score at policy creation time
```

### Reputation Decay Math (ReputationDecay)

```
S(t) = max(S₀ - floor((max(0, t - t_last - t_grace) / 86400)) × r, S_min)
where:
  S₀       = score at last activity
  t_grace  = decayGracePeriod (default 7 days)
  r        = decayRatePerDay (default 1 point/day)
  S_min    = minDecayedScore (default 5)
```

### Recovery Bonus Math (ReputationDecay)

```
recoveryBonus = floor((50 - S) × recoveryBonusMax / 50)   if S < 50
totalBoost    = weight + recoveryBonus
newScore      = min(S + totalBoost, 100)
```

### Scoped Delegation Expiry Math (ScopedDelegation)

```
For sub-delegation at time T₁ from parent with expiresAt = T₀ + D:
  max allowed duration = (T₀ + D) - T₁
  require(duration <= (parent.expiresAt - block.timestamp))
```

---

## Security Invariants

### Cross-Contract Invariants

1. **ERC-8004 is the universal trust anchor.** Every VKP contract gates access on `IVaultfireIdentity.getAgent()`. An unregistered address cannot own a `KeyVault`, create sessions, create delegations, or create scopes.

2. **Zero address is universally blocked.** All constructors and key functions check `!= address(0)`.

3. **CEI pattern in all value-transferring functions.** In `KeyVault.execute()`, `AgentInsurancePool.resolveClaim()`, `AgentSLAEnforcer.expireSLA()`, and `AgentInsurancePool.resolveClaim()`: state is updated before the `.call{value: ...}` interaction.

4. **External call failures use `try/catch` where auto-revocation semantics are required.** `DelegationRegistry._verifyTrustRequirements()` uses `try/catch` specifically to ensure a failing external call commits the revocation rather than rolling it back.

5. **Revocations are irreversible.** Session keys (`SessionKeyManager`), delegations (`DelegationRegistry`), and scopes (`ScopedDelegation`) can be revoked but never un-revoked. New instances must be created.

### Session Key Security

- Session keys expire by timestamp — no on-chain action needed; `validateSession()` reverts automatically.
- Lifetime `totalLimit` enforces a hard cap on aggregate spend regardless of time.
- Owner can revoke all sessions in a single transaction (`revokeAllSessions()`).
- `sessionToId` reverse map overwrites on address reuse — applications must use unique session addresses per session.

### Delegation Security

- Acceptance is two-phase: create (pending) → accept (active). Neither phase is usable without the other.
- `verifyTrust()` is permissionless: watchdog bots can enforce trust requirements without relying on the protocol team.
- Bond and reputation checks in `_verifyTrustRequirements()` use fail-closed `try/catch`.

### Replay Protection

- **Session keys:** `revoked` flag + timestamp + lifetime spend cap.
- **Delegations:** `revoked` flag + timestamp + `accepted` flag.
- **Scopes:** `revoked` + `active` flags + timestamp.
- **SLA reports:** `periodEnd <= block.timestamp` prevents future-dated reports; `periodEnd - periodStart >= MIN_REPORT_PERIOD` prevents zero-duration reports.

---

## Dependencies and Rebuild Recipe

### Dependency Graph

```
Interfaces (no dependencies):
  IVaultfireIdentity.sol
  IVaultfireBonds.sol
  IVaultfireReputation.sol

Core contracts:
  KeyVault.sol              ← IVaultfireIdentity
  VKPFactory.sol            ← KeyVault (and transitively IVaultfireIdentity)
  SessionKeyManager.sol     ← IVaultfireIdentity
  ScopedDelegation.sol      ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  DelegationRegistry.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentSLAEnforcer.sol      ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentInsurancePool.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentSafetyManager.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  ComplianceRegistry.sol    ← IVaultfireIdentity
  ReputationDecay.sol       ← IVaultfireIdentity, IVaultfireReputation

Mocks (test-only):
  MockIdentityRegistry.sol  ← IVaultfireIdentity
  MockBondsRegistry.sol     ← IVaultfireBonds
  MockReputationRegistry.sol← IVaultfireReputation
  ReentrancyAttacker.sol    ← KeyVault
```

### Deployment Order

Deployment order must respect the dependency graph. The three external registries from Batch 3 (ERC-8004) and Batch 2 (Bonds) must already be deployed before any VKP contract is deployed.

```
Step 1 — External prerequisites (already deployed):
  [Batch 3] ERC8004IdentityRegistry → IDENTITY_REGISTRY
  [Batch 2] AIAccountabilityBonds   → BONDS_REGISTRY
  [Batch 3] ERC8004ReputationRegistry → REPUTATION_REGISTRY

Step 2 — VKPFactory (and KeyVault implementation):
  VKPFactory(IDENTITY_REGISTRY)
  → agentVaults, allVaults, vaultCount initialized to empty/0
  → Per-agent KeyVaults deployed on demand via createVault()

Step 3 — SessionKeyManager:
  SessionKeyManager(IDENTITY_REGISTRY)
  → sessionCount = 0

Step 4 — ScopedDelegation:
  ScopedDelegation(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalScopes = 0, owner = deployer

Step 5 — DelegationRegistry:
  DelegationRegistry(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → delegationCount = 0

Step 6 — AgentSLAEnforcer:
  AgentSLAEnforcer(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalSLAs = 0, owner = deployer

Step 7 — AgentInsurancePool:
  AgentInsurancePool(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalPolicies = 0, arbiter = deployer, owner = deployer

Step 8 — AgentSafetyManager:
  AgentSafetyManager(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → safeModeThreshold = 30, freezeThreshold = 10, owner = deployer

Step 9 — ComplianceRegistry:
  ComplianceRegistry(IDENTITY_REGISTRY)
  → totalAttestations = 0, owner = deployer

Step 10 — ReputationDecay:
  ReputationDecay(IDENTITY_REGISTRY, REPUTATION_REGISTRY)
  → decayRatePerDay = 1, decayGracePeriod = 7 days, minDecayedScore = 5

Post-deployment configuration:
  AgentSLAEnforcer.setMonitor(MONITOR_ADDRESS, true)
  AgentInsurancePool.setArbiter(ARBITER_MULTISIG)
  AgentSafetyManager.setSafetyOperator(SAFETY_OPERATOR, true)
  ComplianceRegistry.authorizeAuditor(AUDITOR_ADDRESS, AUDITOR_URI)
  ReputationDecay.setReporter(REPORTER_ADDRESS, true)
  ScopedDelegation.transferOwnership(GOVERNANCE_MULTISIG)
```

### Rebuild Checklist

- [ ] Interfaces deployed as `library`-style references or embedded in consuming contracts (no separate deployment needed for pure interfaces).
- [ ] `VKPFactory` is the canonical entry point for per-agent `KeyVault` deployment. Do not deploy `KeyVault` directly in production — use the factory for discoverability.
- [ ] `AgentInsurancePool.arbiter` should be a multi-sig from day one. The initial deployer-as-arbiter is a temporary convenience.
- [ ] `ComplianceRegistry.authorizeAuditor()` must be called before any attestations can be issued.
- [ ] `ReputationDecay` profiles must be initialized via `initializeProfile()` per agent before `recordActivity()` can be called.
- [ ] Mocks (`MockIdentityRegistry`, `MockBondsRegistry`, `MockReputationRegistry`, `ReentrancyAttacker`) must never appear in production deployment scripts. They are test-only and have no access controls.

---

*End of VaultfireKeyProtocol Technical Reference — Codex Batch 5*


<div style="page-break-before:always"></div>

# Vaultfire Codex — Batch 6: Governance, Privacy, and Miscellaneous Contracts

> Comprehensive line-by-line technical reference for 26 unique contracts covering multisig governance, anti-surveillance enforcement, privacy guarantees, mission enforcement, task escrow, dispute resolution, capability credentials, reputation staking, trust oracle, forum registry, bond insurance, cross-chain trust portability, and supporting utilities.

---

## Table of Contents

1. [MultisigGovernance.sol](#1-multisigsol--contractsmultisiggovernancesol)
2. [MultisigGovernanceV3 (test_fixes)](#2-multisiggovernancesol--test_fixescontractsmultisiggovernancesol)
3. [MultisigGovernanceV3 (vf-audit-contracts)](#3-multisiggovernancesol--vf-audit-contractsmultisiggovernancesol)
4. [MultisigGovernanceV3 (v3-pr-ready)](#4-multisiggovernancesol--v3-pr-readycontractsv3multisiggovernancesol)
5. [push\_contracts\_v3\_MultisigGovernanceV3.sol](#5-push_contracts_v3_multisiggovernancesol)
6. [AntiSurveillance.sol](#6-antisurveillancesol--contractsantisurveillancesol)
7. [PrivacyGuarantees.sol](#7-privacyguaranteessol--contractsprivacyguaranteessol)
8. [MissionEnforcement.sol](#8-missionenforcementsol--contractsmissionenforcementsol)
9. [VaultfireTaskEscrow.sol](#9-vaultfiretaskescrowsol)
10. [VaultfireDisputeResolution.sol](#10-vaultfiredisputeresolutionsol)
11. [VaultfireCapabilityCredentials.sol](#11-vaultfirecapabilitycredentialssol)
12. [VaultfireReputationStaking.sol](#12-vaultfirereputationstakingsol)
13. [VaultfireTrustOracle.sol](#13-vaultfiretrustoracle-sol)
14. [VaultfireForumRegistry.sol](#14-vaultfireforumregistrysol)
15. [VaultfireBondInsurancePool.sol](#15-vaultfirebondinsurancepoolsol)
16. [TrustPortabilityExtensionV3 (push)](#16-trustportabilityextensionv3-push-contractspush_contracts_v3_trustportabilityextensionv3sol)
17. [TrustPortabilityExtensionV3 (workspace root)](#17-trustportabilityextensionv3-workspace-root)
18. [TrustDataBridge.sol](#18-trustdatabridgesol)
19. [IERC20Minimal (vf-audit-contracts)](#19-ierc20minimal--vf-audit-contractsierc20minimalsol)
20. [IERC20Minimal (v3-pr-ready)](#20-ierc20minimal--v3-pr-readycontractsv3ierc20minimalsol)
21. [VaultfireErrors.sol (vf-audit-contracts)](#21-vaultfireerrorssol--vf-audit-contractsvaultfireerrorssol)
22. [VaultfireErrors.sol (test_fixes)](#22-vaultfireerrorssol--test_fixescontractsvaultfireerrorssol)
23. [IVaultfireBridgeNotifier.sol](#23-ivaultfirebridgenotifiersol)
24. [VaultfireInterface.sol](#24-vaultfireinterfacesol)
25. [Deploy.s.sol](#25-deployssol--theloopbreaker-siterisc0scriptdeployssol)
26. [SubmitProof.s.sol](#26-submitproofssol--theloopbreaker-siterisc0scriptsubmitproofssol)
27. [consent\_contract.sol](#27-consent_contractsol--theloopbreaker-siteconsent_contractsol)
28. [RewardStream.t.sol](#28-rewardstreamtsol--theloopbreaker-sitetestrewardstreamtsol)

---

## 1. MultisigGovernance — `contracts/MultisigGovernance.sol`

**Category:** Multisig governance (V1 baseline)

### Full Source

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

// [357 lines — see workspace/contracts/MultisigGovernance.sol]
```

```
Line   1  // SPDX-License-Identifier: MIT
Line   2  pragma solidity ^0.8.25;
Line   4  /**
Line   5   * @title MultisigGovernance
Line   6   * @notice OpenZeppelin-style multisig governance for critical owner functions.
...
Line  21  contract MultisigGovernance {
Line  23  // ============ Errors ============
Line  25  error NotSigner();
Line  26  error ZeroAddress();
Line  27  error InvalidThreshold();
Line  28  error DuplicateSigner();
Line  29  error SignerNotFound();
Line  30  error TransactionNotFound();
Line  31  error TransactionAlreadyExecuted();
Line  32  error TransactionExpired();
Line  33  error AlreadyConfirmed();
Line  34  error NotConfirmed();
Line  35  error ThresholdNotMet();
Line  36  error ExecutionFailed();
Line  37  error CannotRemoveLastSigner();
Line  38  error ThresholdExceedsSigners();
Line  40  // ============ Events ============
Line  72  // ============ Structs ============
Line  81  struct Transaction { address target; uint256 value; bytes data; bool executed;
         uint256 confirmations; uint256 proposedAt; uint256 expiresAt; }
Line  84  // ============ Constants ============
Line  85  uint256 public constant TRANSACTION_EXPIRY = 7 days;
Line  87  // ============ State Variables ============
Line  90  address[] public signers;
Line  93  mapping(address => bool) public isSigner;
Line  96  uint256 public threshold;
Line  99  mapping(uint256 => Transaction) public transactions;
Line 102  mapping(uint256 => mapping(address => bool)) public confirmations;
Line 105  uint256 public transactionCount;
Line 107-156  // ============ Modifiers ============ / constructor
Line 158-239  // ============ Transaction Lifecycle ============
Line 241-288  // ============ Signer Management (via multisig only) ============
Line 290-355  // ============ View Functions ============ + _confirm internal
```

### Line-by-Line Walkthrough

**Lines 1–20 — License, pragma, NatSpec.** MIT license, Solidity 0.8.25. NatSpec describes this as an "OpenZeppelin-style" M-of-N multisig born from a 2026 professional security audit recommendation to replace single-owner control.

**Lines 25–38 — Custom errors.** Fourteen custom errors replace revert strings. All are 4-byte selectors only, eliminating ABI-encoded string payloads from reverts. `CannotRemoveLastSigner` and `ThresholdExceedsSigners` protect the minimum viable signer set.

**Lines 42–74 — Events.** `TransactionProposed` logs the full calldata payload, target, ETH value, and expiry. `TransactionConfirmed` / `ConfirmationRevoked` track individual signer votes. `TransactionExecuted` records the executor. `SignerAdded` / `SignerRemoved` / `ThresholdChanged` audit the signer set.

**Lines 81–88 — Transaction struct.** Packs target, value, calldata blob, execution flag, cumulative confirmation count, proposal timestamp, and expiry into a single storage record keyed by sequential `txId`.

**Line 85 — TRANSACTION_EXPIRY constant.** Fixed at 7 days. Any proposal that accumulates enough signatures but is not executed within 7 days of proposal reverts on `executeTransaction`.

**Lines 90–105 — State variables.** `signers` is a dynamic array for enumeration. `isSigner` map provides O(1) membership lookup without iterating the array. `threshold` is the M in M-of-N. `transactions` maps IDs to structs. `confirmations` is a two-dimensional map: `txId → signer → bool`, preventing double-counting. `transactionCount` is the monotonically incrementing ID counter.

**Lines 109–132 — Modifiers.** `onlySigner` reverts with `NotSigner()` if `isSigner[msg.sender]` is false. `onlySelf` requires `msg.sender == address(this)` — the only way to reach it is via a transaction that the multisig itself executes. `txExists` bounds-checks against `transactionCount`. `notExecuted` rejects replay of already-run transactions. `notExpired` compares `block.timestamp > expiresAt`.

**Lines 139–156 — Constructor.** Validates that at least one signer is provided and that `threshold ∈ [1, len(signers)]`. Iterates signers, rejecting zero-address or duplicates, registers each into `isSigner` and `signers`, emits `SignerAdded` for each.

**Lines 165–191 — proposeTransaction.** Any signer may propose. Increments `transactionCount` (post-increment, so first txId = 0). Writes the `Transaction` struct. Auto-calls `_confirm` so the proposer's own signature is counted immediately — proposal and first confirmation are atomic. Note: this version does **not** track a `pendingTxIds` list (a gap corrected in V3).

**Lines 195–203 — confirmTransaction.** Delegates entirely to `_confirm` after modifier guards. `_confirm` enforces uniqueness via `confirmations[txId][msg.sender]`.

**Lines 207–219 — revokeConfirmation.** Notably **lacks** `notExpired` guard in V1 — a signer could revoke after expiry, though this has no practical effect since execution also checks expiry. Fixed in V3 (see `MultisigGovernanceV3`).

**Lines 223–239 — executeTransaction.** Checks threshold, marks `executed = true` before the external call (CEI pattern), then calls `txn.target.call{value: txn.value}(txn.data)`. Uses `revert ExecutionFailed()` on failure.

**Lines 245–276 — addSigner / removeSigner.** Both are `onlySelf` — they can only be reached through a multisig-approved transaction targeting `address(this)`. `removeSigner` protects against leaving zero signers (`CannotRemoveLastSigner`) and against creating a threshold that exceeds the new signer count (`ThresholdExceedsSigners`). Uses swap-and-pop for gas-efficient array removal. **Ghost-confirmation vulnerability:** removed signer's confirmations on pending transactions are **not** purged in V1 (patched in V3 via `pendingTxIds` sweep).

**Lines 281–288 — changeThreshold.** `onlySelf`. Allows any threshold from 1 to `signers.length`. V1 allows threshold = 1 — a known weakness addressed by `MINIMUM_THRESHOLD = 2` in V3.

**Lines 344–356 — _confirm / receive.** Internal helper sets `confirmations[txId][msg.sender] = true`, increments counter, emits `TransactionConfirmed`. The `receive()` fallback accepts ETH silently to allow the multisig to hold and forward ETH in proposals.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `signers` | `address[]` | Enumerable signer array | Mutable via `addSigner`/`removeSigner` (self-only) |
| `isSigner` | `mapping(address→bool)` | O(1) signer membership | Mutable via signer management |
| `threshold` | `uint256` | Required confirmations (M) | Mutable via `changeThreshold` (self-only) |
| `transactions` | `mapping(uint256→Transaction)` | All proposals | Append-only writes (executed flag flipped on run) |
| `confirmations` | `mapping(uint256→mapping(address→bool))` | Per-signer confirmation flags | Set true on confirm, false on revoke |
| `transactionCount` | `uint256` | Monotonic ID counter | Incremented on each proposal |

### Functions — Full Behavioral Spec

**`proposeTransaction(target, value, data) → txId`**
- Access: `onlySigner`
- Reverts: `ZeroAddress` if target is address(0)
- Effect: creates `Transaction` record, auto-confirms for proposer
- Returns: sequential `txId` starting at 0

**`confirmTransaction(txId)`**
- Access: `onlySigner`
- Guards: `txExists`, `notExecuted`, `notExpired`
- Effect: sets confirmation flag, increments count, emits `TransactionConfirmed`
- Reverts: `AlreadyConfirmed` if already signed

**`revokeConfirmation(txId)`**
- Access: `onlySigner`
- Guards: `txExists`, `notExecuted` (but NOT `notExpired` in V1)
- Effect: clears flag, decrements count, emits `ConfirmationRevoked`
- Reverts: `NotConfirmed` if never confirmed

**`executeTransaction(txId)`**
- Access: `onlySigner`
- Guards: `txExists`, `notExecuted`, `notExpired`
- Effect: marks executed, performs external call, reverts on call failure
- CEI: `executed = true` set before `.call`

**`addSigner(signer)`**
- Access: `onlySelf` (only reachable via multisig proposal)
- Reverts: `ZeroAddress`, `DuplicateSigner`
- Effect: adds to `signers` array and `isSigner` map

**`removeSigner(signer)`**
- Access: `onlySelf`
- Reverts: `SignerNotFound`, `CannotRemoveLastSigner`, `ThresholdExceedsSigners`
- Effect: swap-and-pop from `signers`, clears `isSigner` — does NOT purge ghost confirmations

**`changeThreshold(newThreshold)`**
- Access: `onlySelf`
- Reverts: `InvalidThreshold` if 0 or > `signers.length`
- Effect: updates `threshold`, emits `ThresholdChanged`

### Math

**Confirmation threshold check:** `confirmations >= threshold`

No BPS scaling. Threshold is an absolute count. Execution requires `signatures_count >= threshold`.

### Events

| Event | Trigger | Data Logged |
|---|---|---|
| `TransactionProposed` | `proposeTransaction` | txId, proposer, target, data, value, expiresAt |
| `TransactionConfirmed` | `_confirm` (via propose or confirm) | txId, signer |
| `ConfirmationRevoked` | `revokeConfirmation` | txId, signer |
| `TransactionExecuted` | `executeTransaction` | txId, executor |
| `SignerAdded` | `addSigner` + constructor | signer address |
| `SignerRemoved` | `removeSigner` | signer address |
| `ThresholdChanged` | `changeThreshold` | old and new threshold |

### Security Invariants

1. `threshold ≥ 1` at all times (enforced by constructor + `changeThreshold`)
2. `signers.length ≥ 1` always (enforced by `CannotRemoveLastSigner`)
3. `threshold ≤ signers.length` always
4. Executed transactions cannot be re-executed (`notExecuted` guard)
5. Expired transactions cannot be executed (`notExpired` guard)
6. Double-confirming is prevented (`AlreadyConfirmed`)
7. **Known gap:** ghost confirmations from removed signers persist (patched in V3)
8. **Known gap:** `revokeConfirmation` missing `notExpired` guard (patched in V3)
9. **Known gap:** threshold = 1 allowed (patched in V3 with `MINIMUM_THRESHOLD = 2`)
10. **Known gap:** no cap on signer count (patched in V3 with `MAX_SIGNERS = 20`)

### Integration with Vaultfire

`MultisigGovernance` is the foundational governance contract. `VaultfireTaskEscrow` stores its address as the immutable `governance` and only allows `resolveDispute` to be called by this address. Bond contracts' `onlyOwner` functions are intended to be owned by a deployed multisig instance. This V1 contracts is superseded by `MultisigGovernanceV3` but remains deployed as the baseline reference.

### Dependencies + Rebuild Recipe

- **Dependencies:** None — zero imports
- **Solidity:** `^0.8.25`
- **Deploy:** `constructor(address[] _signers, uint256 _threshold)`
- **Test suite file:** No dedicated test file in this batch (governance tests live in `AuditRemediation.test.js`)

---

## 2. MultisigGovernanceV3 — `test_fixes/contracts/MultisigGovernanceV3.sol`

**Category:** Multisig governance (V3 timelock variant with guardians)

### Overview

This is the test-fixes branch of the V3 multisig. It introduces a fundamentally different architecture from V1: a **timelock model** rather than a simple threshold-counter. Proposals must wait `EXECUTION_TIMELOCK = 24 hours` before execution regardless of signature count. The guardian system adds a 3-of-3 emergency bypass. Signer rotation is governance-gated via a separate `SignerChangeRequest` flow with its own 7-day cooldown.

### Full Source (line-by-line)

```
 1  // SPDX-License-Identifier: MIT
 2  pragma solidity ^0.8.25;
 4  import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
 6–9   NatSpec: V3 — threshold enforcement, time-locked execution, signer rotation limits, expiry
10  contract MultisigGovernanceV3 is ReentrancyGuard {
12  uint256 public constant EXECUTION_TIMELOCK     = 24 hours;
13  uint256 public constant PROPOSAL_EXPIRY        = 7 days;
14  uint256 public constant SIGNER_CHANGE_COOLDOWN = 7 days;
15  uint256 public constant MIN_THRESHOLD_BPS      = 5000; // 50%
17–30  Custom errors (13 total)
32–41  struct Proposal { bytes data; address target; uint256 value; uint64 createdAt;
        uint64 executableAt; uint256 signatures; bool executed; uint256 nonce; }
43  mapping(uint256 => Proposal)                     public proposals;
44  mapping(uint256 => mapping(address => bool))     public hasSigned;
45  mapping(address => bool)                         public isSigner;
46  address[3]                                       public guardians;
47  address[]                                        public signers;
48  uint256                                          public threshold;
49  uint256                                          public proposalCount;
50  uint256                                          public globalNonce;
51  uint256                                          public lastSignerChange;
54–55  mapping(bytes32 => mapping(address => bool)) public guardianApprovals;
       mapping(bytes32 => uint256)                  public guardianApprovalCount;
57–64  Events (ProposalCreated, ProposalSigned, ProposalExecuted, SignerAdded,
        SignerRemoved, ThresholdUpdated, EmergencyApproved, EmergencyExecuted)
66–78  constructor: validates signers, threshold ≥ 1, MIN_THRESHOLD_BPS, stores guardians
80–83  modifier onlySigner
85–102 createProposal: assigns executableAt = now + EXECUTION_TIMELOCK, nonce = ++globalNonce
104–112 signProposal: nonReentrant, checks expiry, no double-sign, increments p.signatures
114–126 executeProposal: nonReentrant, checks timelock, expiry, threshold, CEI flag, .call
128    error SignerChangeRequiresThreshold();
130–136 struct SignerChangeRequest { address add; address remove; uint64 proposedAt;
         uint256 approvals; bool executed; }
138–140 mappings for signerChanges, signerChangeApprovals, signerChangeCount
142–144 events SignerChangeProposed, SignerChangeApproved, SignerChangeExecuted
148–166 proposeSignerChange: cooldown gate, duplicate/zero checks, starts approval at 1
168–177 approveSignerChange: checks expiry, no double-approval
179–206 executeSignerChange: requires threshold approvals + EXECUTION_TIMELOCK + expiry
         swap-and-pop for removal, then push new signer, sets lastSignerChange
208–228 emergencyExecute: all-3-guardian check, callHash deduplication, execute
230–232 getProposalStatus: view returns full Proposal struct
234    receive() external payable
```

### Line-by-Line Walkthrough

**Line 4 — OZ ReentrancyGuard.** Imported from OpenZeppelin. Provides `nonReentrant` modifier via a slot-based lock (`_status`).

**Lines 12–15 — Constants.**
- `EXECUTION_TIMELOCK = 24 hours` — every proposal must wait at minimum 24 hours after creation before `executeProposal` can run. Gives signers a window to revoke if a bad proposal slips through.
- `PROPOSAL_EXPIRY = 7 days` — proposals that are not executed within 7 days become permanently invalid.
- `SIGNER_CHANGE_COOLDOWN = 7 days` — prevents rapid successive signer rotations.
- `MIN_THRESHOLD_BPS = 5000` — threshold must be ≥ 50% of the signer count at all times.

**Lines 17–30 — Errors.** Thirteen custom errors. `SignerChangeCooldown`, `NotGuardian`, `GuardianAlreadyApproved` are new versus V1. `AddressAlreadySigner` is reused (confusingly) to indicate a zero address was passed to `proposeSignerChange` — a minor naming ambiguity.

**Lines 32–41 — Proposal struct.** Uses `uint64` for `createdAt` and `executableAt` to pack timestamps into 8 bytes each, saving one storage slot versus two `uint256`s. The `nonce` field provides global sequencing for off-chain tooling.

**Line 46 — guardians[3].** Fixed-size array of exactly 3 guardian addresses. The emergency execute path requires all three to sign off.

**Lines 66–78 — Constructor.** Enforces `_threshold * 10000 / _signers.length >= MIN_THRESHOLD_BPS` — e.g., with 4 signers the threshold must be ≥ 2. Does not check for duplicate signers in the input array (a minor gap).

**Lines 85–102 — createProposal.** `onlySigner`. `executableAt = block.timestamp + EXECUTION_TIMELOCK`. Assigns `nonce = ++globalNonce` for off-chain ordering. Returns `proposalId` (starts at 1 due to `++proposalCount`).

**Lines 104–112 — signProposal.** `nonReentrant`. Checks `createdAt != 0` (existence), `createdAt + PROPOSAL_EXPIRY > now` (not expired), `!hasSigned[id][msg.sender]` (no double-sign). Increments `p.signatures`.

**Lines 114–126 — executeProposal.** `nonReentrant`. Checks `!p.executed`, `now >= p.executableAt` (timelock), `now <= createdAt + PROPOSAL_EXPIRY` (not expired), `p.signatures >= threshold`. Sets `p.executed = true` **before** the `.call` (CEI). Note: this variant does NOT have the MED-21 post-execution threshold re-check that the audit-contracts variant adds.

**Lines 148–166 — proposeSignerChange.** Guards against the `SIGNER_CHANGE_COOLDOWN`. The `add == address(0)` revert uses `AddressAlreadySigner` (semantically imprecise but functionally correct). Starts with `approvals = 1` from the proposer.

**Lines 179–206 — executeSignerChange.** Requires `sc.approvals >= threshold`. Also enforces `EXECUTION_TIMELOCK` on signer changes. This means a rotation always takes at least 24 hours to execute after proposal, in addition to requiring threshold approvals.

**Lines 208–228 — emergencyExecute.** Iterates `guardians[3]` to verify caller. Uses `keccak256(abi.encode(target, data))` as the `callHash` to identify the proposed action. Each guardian independently calls `emergencyExecute`; on the third approval, the call fires. **Critical note:** this variant does NOT have the CRIT-01 fix (`executedCallHashes`) — meaning after all 3 guardians approve, `guardianApprovalCount` stays at 3 permanently and any guardian can re-trigger execution. This is the unpatched version.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `proposals` | `mapping(uint256→Proposal)` | All proposal records | Written on create, `executed` flag on run |
| `hasSigned` | `mapping(uint256→mapping(address→bool))` | Per-signer sign flags | Set true on `signProposal` |
| `isSigner` | `mapping(address→bool)` | Fast membership check | Changed by `executeSignerChange` |
| `guardians` | `address[3]` | Fixed 3-element guardian array | Set in constructor, immutable thereafter |
| `signers` | `address[]` | Enumerable signer list | Changed by `executeSignerChange` |
| `threshold` | `uint256` | Required signatures | Not externally settable post-deployment in this version |
| `proposalCount` | `uint256` | Next proposal ID (1-based) | Incremented on each `createProposal` |
| `globalNonce` | `uint256` | Monotonic nonce for sequencing | Incremented each proposal |
| `lastSignerChange` | `uint256` | Timestamp of last rotation | Set on `executeSignerChange` |
| `guardianApprovals` | `mapping(bytes32→mapping(address→bool))` | Per-guardian approval per callHash | Set by each guardian call |
| `guardianApprovalCount` | `mapping(bytes32→uint256)` | Total approvals per callHash | Incremented per guardian |

### Functions

**`createProposal(target, value, data) → proposalId`**
- Creates proposal with `executableAt = now + 24h`, `nonce = ++globalNonce`
- No auto-confirmation for proposer (unlike V1 — signer must separately call `signProposal`)

**`signProposal(proposalId)`**
- `onlySigner`, `nonReentrant`
- Validates existence, non-expiry, no double-sign
- Increments signature counter

**`executeProposal(proposalId)`**
- `nonReentrant`, open to any caller
- Validates all conditions, CEI pattern, forwards ETH value

**`proposeSignerChange(add, remove) → changeId`**
- `onlySigner`, subject to 7-day cooldown
- Starts approval at 1

**`approveSignerChange(changeId)`**
- `onlySigner`, validates non-expiry and no double-approval

**`executeSignerChange(changeId)`**
- `onlySigner`, `nonReentrant`
- Requires threshold approvals and 24h timelock
- Removes old signer via swap-and-pop, adds new

**`emergencyExecute(data, target)`**
- Open to guardians only
- Accumulates approvals; fires on third
- **Unpatched replay risk** — no `executedCallHashes` guard

**`getProposalStatus(proposalId) → Proposal`** — view, returns full struct

### Math

**Threshold enforcement at construction:**
```
require(_threshold * 10000 / _signers.length >= 5000)
```
This is integer arithmetic. For `_signers.length = 2`, `_threshold = 1`: `1 * 10000 / 2 = 5000 ≥ 5000` — passes. For `_signers.length = 3`, `_threshold = 1`: `1 * 10000 / 3 = 3333 < 5000` — fails.

**Emergency path:** requires `guardianApprovalCount[callHash] >= 3`.

### Events

| Event | Trigger | Key Fields |
|---|---|---|
| `ProposalCreated` | `createProposal` | proposalId, target, value |
| `ProposalSigned` | `signProposal` | proposalId, signer |
| `ProposalExecuted` | `executeProposal` | proposalId |
| `SignerAdded/Removed` | `executeSignerChange` | signer address |
| `ThresholdUpdated` | N/A in this version | — |
| `EmergencyApproved` | each guardian call | callHash, guardian, approvalCount |
| `EmergencyExecuted` | third guardian approval | callHash, target |
| `SignerChangeProposed/Approved/Executed` | signer change flow | changeId, add, remove |

### Security Invariants

1. Proposals require 24-hour timelock before execution
2. Proposals expire after 7 days
3. Threshold must be ≥ 50% of signer count (enforced at construction only)
4. Signer changes require threshold approvals + timelock + cooldown
5. Only the 3 fixed guardians can use `emergencyExecute`
6. **UNPATCHED:** `emergencyExecute` lacks `executedCallHashes` replay guard (CRIT-01)
7. **UNPATCHED:** no post-execution threshold floor check on self-call proposals (MED-21)
8. Reentrancy on `signProposal` and `executeProposal` blocked by OZ `ReentrancyGuard`

### Integration

This variant is used in test environments. Production deployments should use the audit-contracts variant (`vf-audit-contracts/MultisigGovernanceV3.sol`) which contains the CRIT-01 and MED-21 fixes.

### Dependencies + Rebuild Recipe

- **Import:** `@openzeppelin/contracts/utils/ReentrancyGuard.sol`
- **Constructor:** `(address[] _signers, uint256 _threshold, address[3] _guardians)`
- **Minimum viable deploy:** 2 signers with threshold=1 fails (MIN_THRESHOLD_BPS), minimum is 2-of-2

---

## 3. MultisigGovernanceV3 — `vf-audit-contracts/MultisigGovernanceV3.sol`

**Category:** Multisig governance (V3 with CRIT-01 and MED-21 audit fixes applied)

### Overview

Identical architecture to the test-fixes variant above, but with two critical audit remediations applied:

**CRIT-01 fix (lines 241–246):** `executedCallHashes` mapping tracks which `callHash`es have already been executed via `emergencyExecute`. The check-and-set is performed **before** the `.call` (strict CEI), preventing indefinite re-execution by guardians after the count reaches 3.

**MED-21 fix (lines 134–139):** After any proposal that targets `address(this)` executes (a self-call), the contract re-checks `threshold * 10000 / signers.length >= MIN_THRESHOLD_BPS`. This prevents a colluding majority from using a self-call proposal to lower the threshold below 50%, circumventing the construction-time invariant.

### Additional State Variable vs test-fixes

```solidity
// Line 59
mapping(bytes32 => bool) public executedCallHashes; // [audit-v3.3] CRIT-01 fix
```

### Modified emergencyExecute (lines 224–251)

```
Line 241  require(!executedCallHashes[callHash], "already executed");
Line 246  executedCallHashes[callHash] = true;  // EFFECT before INTERACTION
Line 248  (bool ok,) = target.call(data);
```

### Modified executeProposal (lines 118–142)

```
Line 134  if (p.target == address(this)) {
Line 136      require(threshold * 10000 / signers.length >= MIN_THRESHOLD_BPS, "threshold below minimum");
Line 139  }
```

All other behaviors, state variables, functions, math, and events are identical to the test-fixes variant documented above.

### Security Invariants (additions)

9. `executedCallHashes` prevents `emergencyExecute` replay (CRIT-01)
10. Self-call proposals cannot lower threshold below 50% (MED-21)

### This is the Canonical Production Variant

The `vf-audit-contracts/` directory contains the versions submitted for professional audit. This is the authoritative production-ready `MultisigGovernanceV3`.

---

## 4. MultisigGovernanceV3 — `v3-pr-ready/contracts/v3/MultisigGovernanceV3.sol`

**Category:** Multisig governance (V3 redesigned — "clean room" PR-ready variant)

### Overview

This is a substantially different architecture from the timelock variants above. It uses a **classic Gnosis Safe-style** confirmation model: no mandatory timelock, confirmations accumulate from signers, threshold triggers immediate execution readiness. Key additions from a 2026 audit:

- `MINIMUM_THRESHOLD = 2` — eliminates 1-of-N setups
- `MAX_SIGNERS = 20` — caps signer set size
- `pendingTxIds` array with O(1) removal — enables ghost-confirmation purge when a signer is removed
- `_pendingIndex` map for O(1) lookup into `pendingTxIds`
- `ETHReceived` event in `receive()`
- `OnlySelf` custom error replacing the `require` string in `onlySelf`
- `notExpired` added to `revokeConfirmation`
- `proposedAt` added to `getTransaction` return tuple

### Full Source Summary

```
Line  1–22   License, pragma, NatSpec (design notes, audit fix references)
Line 23       contract MultisigGovernanceV3 {
Line 25–44   Errors — 13 including MaxSignersReached, OnlySelf
Line 46–77   Events — includes ETHReceived
Line 79–89   struct Transaction { target, value, data, executed, confirmations, proposedAt, expiresAt }
Line 91–100  Constants: TRANSACTION_EXPIRY=7days, MINIMUM_THRESHOLD=2, MAX_SIGNERS=20
Line 102–128 State: signers[], isSigner, threshold, transactions, confirmations,
              transactionCount, pendingTxIds[], _pendingIndex
Line 130–156 Modifiers: onlySigner, onlySelf, txExists, notExecuted, notExpired
Line 158–187 Constructor: enforces MINIMUM_THRESHOLD, MAX_SIGNERS, dup-check
Line 189–280 Transaction lifecycle: propose (adds to pendingTxIds, auto-confirm),
              confirm, revoke (now notExpired), execute (removes from pendingTxIds)
Line 282–346 Signer management: addSigner (MAX_SIGNERS cap), removeSigner (ghost-purge sweep)
Line 348–361 changeThreshold: enforces MINIMUM_THRESHOLD
Line 363–423 View functions: getSigners, getSignerCount, getTransaction (+proposedAt),
              isTransactionReady, getPendingTxIds
Line 425–464 Internal: _confirm, _addToPending, _removeFromPending, _removeFromPendingAt
Line 466–471 receive() with ETHReceived event
```

### Critical Architecture: Ghost-Confirmation Purge

When `removeSigner(signer)` is called, the contract iterates the entire `pendingTxIds` array in reverse:

```solidity
// Lines 323–343
while (i > 0) {
    --i;
    uint256 txId = pendingTxIds[i];
    Transaction storage txn = transactions[txId];
    // Opportunistically clean expired entries
    if (block.timestamp > txn.expiresAt) {
        _removeFromPendingAt(i);
        continue;
    }
    // Revoke ghost confirmation
    if (confirmations[txId][signer]) {
        confirmations[txId][signer] = false;
        txn.confirmations--;
    }
}
```

This is an O(P) operation where P = number of pending transactions. In the worst case with many pending transactions, `removeSigner` could be expensive. The 20-signer cap and 7-day expiry limit practical P.

### pendingTxIds O(1) Management

```
_addToPending(txId):
    _pendingIndex[txId] = pendingTxIds.length
    pendingTxIds.push(txId)

_removeFromPendingAt(idx):
    movedTxId = pendingTxIds[last]
    pendingTxIds[idx] = movedTxId
    _pendingIndex[movedTxId] = idx
    pendingTxIds.pop()
```

Standard swap-and-pop with index maintenance. Removal is O(1).

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `signers` | `address[]` | Enumerable signer list | Via `addSigner`/`removeSigner` (self-only) |
| `isSigner` | `mapping(address→bool)` | O(1) membership | Signer management |
| `threshold` | `uint256` | Min confirmations, ≥ 2 | `changeThreshold` (self-only) |
| `transactions` | `mapping(uint256→Transaction)` | Proposal records | Written on propose, flag on execute |
| `confirmations` | `mapping(uint256→mapping(address→bool))` | Vote tracking | Confirm/revoke/ghost-purge |
| `transactionCount` | `uint256` | Next txId (post-increment) | Increments on propose |
| `pendingTxIds` | `uint256[]` | Active (non-executed) proposal IDs | Add on propose, remove on execute/expire |
| `_pendingIndex` | `mapping(uint256→uint256)` | txId → position in pendingTxIds | Managed by _addToPending/removePending |

### Math

**Minimum threshold:** `threshold >= MINIMUM_THRESHOLD = 2` at all times.

**Threshold bounds:** `2 ≤ threshold ≤ signers.length ≤ 20`

**Ghost purge cost:** O(pendingTxIds.length) per `removeSigner`

### Events (additional vs V1)

| Event | Trigger | Purpose |
|---|---|---|
| `ETHReceived` | `receive()` | Audit trail for incoming ETH |

### Security Invariants

1. `threshold ≥ 2` enforced by constructor and `changeThreshold`
2. `signers.length ≤ 20` enforced by `addSigner`
3. Ghost confirmations purged on `removeSigner`
4. Expired pending transactions opportunistically cleaned during `removeSigner`
5. `revokeConfirmation` now also checks `notExpired`
6. No guardian / emergency path in this variant (simpler design)
7. `onlySelf` uses custom error `OnlySelf` not a require string

---

## 5. `push_contracts_v3_MultisigGovernanceV3.sol`

**Category:** Multisig governance (push branch — identical to early V3 timelock, pre-CRIT-01 fix)

### Overview

This is the version pushed to an early `push_contracts_v3` branch. It is identical to `test_fixes/contracts/MultisigGovernanceV3.sol` in structure and behavior. The `emergencyExecute` path lacks `executedCallHashes` (CRIT-01 still present). The `proposeSignerChange` collapses the add/remove into a single atomic function call (no separate approval flow) — the proposer atomically executes the swap:

```solidity
// Lines 128–151: proposeSignerChange is a direct atomic swap
function proposeSignerChange(address add, address remove) external onlySigner {
    // ...cooldown, dup checks...
    isSigner[remove] = false;  // remove immediately
    // swap-and-pop signers[]
    isSigner[add] = true;      // add immediately
    signers.push(add);
    lastSignerChange = block.timestamp;
}
```

This means a single signer can rotate the set without threshold approval — a vulnerability that the `test_fixes` and `vf-audit-contracts` variants address by converting `proposeSignerChange` into a multi-step approval flow (`proposeSignerChange` → `approveSignerChange` × N → `executeSignerChange`).

**Differences from patched variants:**
- No `SignerChangeRequiresThreshold` error
- No `SignerChangeRequest` struct
- No `signerChanges` / `signerChangeApprovals` / `signerChangeCount` state
- `proposeSignerChange` has no return value and executes atomically
- `emergencyExecute` has no `executedCallHashes` replay guard

**All other behaviors** (constants, Proposal struct, `createProposal`, `signProposal`, `executeProposal`, `getProposalStatus`) are identical to the test-fixes variant.

### Security Invariants — Gaps

- **HIGH-MULTISIG-01:** Any single signer can unilaterally rotate the signer set (patched in test_fixes)
- **CRIT-01:** `emergencyExecute` replay (patched in vf-audit-contracts)

---

## 6. AntiSurveillance — `contracts/AntiSurveillance.sol`

**Category:** Anti-surveillance enforcement

### Full Source Summary

```
  1  // SPDX-License-Identifier: MIT
  2  pragma solidity ^0.8.25;
  4–17  NatSpec: privacy-preserving by design, no freeform strings, non-binding reports,
         owner/governance controlled enforcement
18  contract AntiSurveillance {
19–26  address public owner; events OwnershipTransferred; modifier onlyOwner
33–36  string public constant ANTI_SURVEILLANCE_COMMITMENT
42–52  enum BannedDataType { BEHAVIORAL_TRACKING, CROSS_PROTOCOL_LINKING,
        METADATA_HARVESTING, BIOMETRIC_DATA, LOCATION_DATA, DEVICE_FINGERPRINTING,
        SOCIAL_GRAPH_MINING, SENTIMENT_ANALYSIS, PREDICTIVE_PROFILING }
57–64  mapping(address => uint256) public surveillanceFreeModules
        mapping(address => bool) public bannedForSurveillance
66–81  Events: ModuleVerifiedSurveillanceFree, ModuleBannedForSurveillance
        (uses hashes not strings), SurveillanceAttemptBlocked
83–87  modifier surveillanceFreeOnly(address module)
89–91  constructor: owner = msg.sender
93–98  transferOwnership
101–113 reportSurveillanceViolation: permissionless, non-binding, emits with zero reasonHash
116–125 banModuleForSurveillance: onlyOwner, binding, sets bannedForSurveillance[module]=true
128–130 unbanModule: onlyOwner
132–142 verifyModuleSurveillanceFree: onlyOwner, sets timestamp
144–163 getAntiSurveillancePolicy, isDataTypeBanned, getBannedDataTypes (pure views)
```

### Line-by-Line Walkthrough

**Lines 33–36 — ANTI_SURVEILLANCE_COMMITMENT constant.** Immutable string encoded in bytecode. Cannot be changed post-deployment. Acts as an on-chain policy declaration.

**Lines 42–52 — BannedDataType enum.** Defines nine categories of prohibited data collection. Every value maps to a `uint8` (0–8). Used in events to log violation type without emitting freeform text.

**Lines 57–64 — State.** `surveillanceFreeModules` stores the `block.timestamp` of verification (non-zero = verified). `bannedForSurveillance` is a simple boolean flag. Once banned, `verifyModuleSurveillanceFree` cannot be called on that module (`require(!bannedForSurveillance[module])`).

**Lines 66–81 — Events.** `ModuleBannedForSurveillance` accepts `reasonHash` and `evidenceHash` instead of strings — a deliberate privacy choice. Off-chain systems publish the signed report; on-chain stores only the hash. `SurveillanceAttemptBlocked` is defined but never emitted in this contract — intended for composing modules that inherit `surveillanceFreeOnly`.

**Lines 83–87 — surveillanceFreeOnly modifier.** Dual check: module must be verified AND not subsequently banned. Intended for use by other contracts that want to gate operations on surveillance-free module status.

**Lines 101–113 — reportSurveillanceViolation.** Permissionless. Anyone can call. Non-binding — emits the event with `reasonHash = bytes32(0)` (unknown/unspecified). This is explicitly designed to be non-binding to prevent griefing/DoS attacks.

**Lines 116–125 — banModuleForSurveillance.** `onlyOwner`. Binding governance action. Takes both `reasonHash` and `evidenceHash` (both must be non-zero). Sets `bannedForSurveillance[module] = true` permanently until `unbanModule`.

**Lines 153–155 — isDataTypeBanned.** Returns `true` for ALL data types — the comment explicitly states `dataType; // suppress unused variable warning`. This is intentional: all 9 banned data types are always banned.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `owner` | `address` | Governance authority | Changed by `transferOwnership` |
| `surveillanceFreeModules` | `mapping(address→uint256)` | Verification timestamp (0 = not verified) | Set by `verifyModuleSurveillanceFree` |
| `bannedForSurveillance` | `mapping(address→bool)` | Module ban flag | Set/cleared by owner |

### Functions

| Function | Access | Effect |
|---|---|---|
| `transferOwnership(newOwner)` | `onlyOwner` | Atomically transfers ownership, no two-step |
| `reportSurveillanceViolation(module, type, evidenceHash)` | Public | Emits event only, non-binding |
| `banModuleForSurveillance(module, type, reasonHash, evidenceHash)` | `onlyOwner` | Sets ban flag, emits event |
| `unbanModule(module)` | `onlyOwner` | Clears ban flag |
| `verifyModuleSurveillanceFree(module)` | `onlyOwner` | Sets timestamp, cannot re-verify banned modules |
| `getAntiSurveillancePolicy()` | `pure` | Returns policy string |
| `isDataTypeBanned(dataType)` | `pure` | Always returns true |
| `getBannedDataTypes()` | `pure` | Returns human-readable list |

### Events

| Event | When Fired | Logged Data |
|---|---|---|
| `ModuleVerifiedSurveillanceFree` | `verifyModuleSurveillanceFree` | module, timestamp |
| `ModuleBannedForSurveillance` | `banModuleForSurveillance` + `reportSurveillanceViolation` | module, violationType, reasonHash, evidenceHash, timestamp |
| `SurveillanceAttemptBlocked` | Defined but never emitted in this contract | attacker, attemptedType, timestamp |
| `OwnershipTransferred` | `transferOwnership` | previous, new |

### Security Invariants

1. All 9 banned data types are permanently banned — `isDataTypeBanned` always returns true
2. Public reports are non-binding — only owner can effect module state changes
3. Evidece is stored as hashes only — no PII written on-chain
4. A banned module cannot be re-verified without unbanning first
5. `transferOwnership` is single-step — use multisig address as owner in production
6. The `surveillanceFreeOnly` modifier is available for composing contracts to gate operations

### Integration

`AntiSurveillance` is used as a registry by protocol modules that want to assert surveillance-free status. The `surveillanceFreeOnly` modifier can be imported/used by bond contracts and the identity registry to gate sensitive operations.

---

## 7. PrivacyGuarantees — `contracts/PrivacyGuarantees.sol`

**Category:** Privacy guarantees (consent and deletion management)

### Full Source Summary

```
  1  // SPDX-License-Identifier: MIT
  2  pragma solidity ^0.8.25;
  4–16  NatSpec: privacy floor for modules, hashed purposes, no freeform strings
17  contract PrivacyGuarantees {
22  string public constant PRIVACY_POLICY_VERSION = "Vaultfire-v1.1-NoKYC-WalletOnly-HashedPurposes"
30  uint256 public constant DATA_RETENTION_PERIOD = 30 days
33  mapping(address => uint256) public deletionRequests
36  mapping(address => mapping(bytes32 => bool)) public userConsent
38–42  Events: DataDeletionRequested, DataPermanentlyDeleted, ConsentGranted, ConsentRevoked
44–52  Modifiers: requiresConsent, checkDeletionRequest
58–60  hashPurpose(purpose) → bytes32: pure helper
66–80  grantConsent(purposeHash), revokeConsent(purposeHash)
87–98  requestDataDeletion(), cancelDeletionRequest()
100–107 hasConsent(), isDeletionEnforceable()
109–115 getPrivacyPolicy() pure
```

### Line-by-Line Walkthrough

**Line 22 — PRIVACY_POLICY_VERSION.** An immutable identifier anchoring this deployment to a specific policy version. If the privacy policy materially changes, a new contract is deployed with a new version string.

**Line 30 — DATA_RETENTION_PERIOD = 30 days.** The period between a user's deletion request and when off-chain systems are required to have completed deletion/redaction. On-chain state is never truly deleted (blockchain immutability), but this signals the 30-day off-chain obligation window.

**Line 33 — deletionRequests.** Maps user address to the `effectiveAt` timestamp (`block.timestamp + 30 days`) of their deletion request, or 0 if no request is pending.

**Line 36 — userConsent.** Double mapping: `user → purposeHash → bool`. Purposes are represented as `keccak256` hashes of human-readable strings. The human-readable strings never touch the chain.

**Lines 44–52 — Modifiers.** `requiresConsent(user, purposeHash)` is designed for use by inheriting contracts to gate data operations. `checkDeletionRequest(user)` prevents new data writes for users who have requested deletion.

**Lines 66–80 — grantConsent / revokeConsent.** Both are `msg.sender`-based — users can only grant/revoke their own consent. `purposeHash != bytes32(0)` prevents vacuous consents.

**Lines 87–93 — requestDataDeletion.** One deletion request per user at a time (idempotency guard). Sets `effectiveAt = now + 30 days`. Emits `DataDeletionRequested` with the effective timestamp.

**Lines 95–98 — cancelDeletionRequest.** Allows user to rescind a pending deletion request before it becomes effective.

**Lines 104–107 — isDeletionEnforceable.** Returns `(true, effectiveAt)` only when `effectiveAt > 0 AND now >= effectiveAt`. Off-chain systems should poll this to know when to execute deletion.

**Note:** `DataPermanentlyDeleted` event is defined but **never emitted** in this contract. It is intended to be emitted by off-chain systems or composing contracts that actively confirm deletion completion.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `deletionRequests` | `mapping(address→uint256)` | effectiveAt timestamp (0 = none) | Set by `requestDataDeletion`, cleared by `cancelDeletionRequest` |
| `userConsent` | `mapping(address→mapping(bytes32→bool))` | Consent flags per purpose | Set/cleared by `grantConsent`/`revokeConsent` |

### Functions

| Function | Access | Effect |
|---|---|---|
| `hashPurpose(purpose)` | `public pure` | `keccak256(abi.encodePacked(purpose))` — helper for UI integration |
| `grantConsent(purposeHash)` | `external` | Sets `userConsent[msg.sender][hash] = true` |
| `revokeConsent(purposeHash)` | `external` | Sets to `false`, emits `ConsentRevoked` |
| `requestDataDeletion()` | `external` | Sets `deletionRequests[msg.sender] = now + 30 days` |
| `cancelDeletionRequest()` | `external` | Clears deletion request |
| `hasConsent(user, purposeHash)` | `public view` | Returns consent flag |
| `isDeletionEnforceable(user)` | `public view` | Returns `(enforceable, effectiveAt)` |
| `getPrivacyPolicy()` | `public pure` | Returns human-readable policy string |

### Events

| Event | When Fired | Data |
|---|---|---|
| `DataDeletionRequested` | `requestDataDeletion` | user, effectiveAt |
| `DataPermanentlyDeleted` | **Never emitted on-chain** — for off-chain/composing contracts | user, timestamp |
| `ConsentGranted` | `grantConsent` | user, purposeHash |
| `ConsentRevoked` | `revokeConsent` | user, purposeHash |

### Security Invariants

1. Users can only manage their own consent — no third-party delegation
2. Deletion requests are idempotent — one pending request per address
3. Purpose hashes prevent PII in consent data
4. `DATA_RETENTION_PERIOD` is immutable — cannot be shortened by governance
5. `PRIVACY_POLICY_VERSION` is immutable — policy changes require new deployment
6. The `DataPermanentlyDeleted` event being off-chain means on-chain state is not erased (by design — blockchain immutability)

### Integration

`PrivacyGuarantees` is a composable base contract. Other modules can `import` it and use `requiresConsent` and `checkDeletionRequest` modifiers to gate data writes. The ERC-8004 identity and reputation registries are expected to check `isDeletionEnforceable` before processing updates for users who have requested deletion.

---

## 8. MissionEnforcement — `contracts/MissionEnforcement.sol`

**Category:** Mission enforcement (protocol-level ethics registry)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
 25  contract MissionEnforcement {
 30–33  address public owner; address public pendingOwner; events; onlyOwner modifier
 47–50  string public constant MISSION_STATEMENT
 56–65  enum CorePrinciple { HUMAN_VERIFICATION_FINAL_SAY, AI_PROFIT_CAPS, PRIVACY_DEFAULT,
         COMMUNITY_CHALLENGES, OPEN_SOURCE_VERIFIABLE, NO_KYC_WALLET_ONLY, NO_DATA_SALE,
         RIGHT_TO_BE_FORGOTTEN }
 71  mapping(address => mapping(CorePrinciple => bool)) public missionCompliance
 76–83  struct MissionReport { reporter, timestamp, principle, evidence }
 83  mapping(address => MissionReport[]) public missionReports
 89  mapping(address => uint256) public missionCompliantModules
 95  mapping(address => CorePrinciple) public missionViolations
100–130 Events: ModuleMissionCompliant, MissionViolationReported, MissionViolationDetected,
         MissionBypassAttemptBlocked
135–138 modifier enforceMission(address module, CorePrinciple principle)
147–157 verifyAIProfitCap(aiShare, totalAmount, capPercentage) → bool (pure math)
167–178 verifyHumanFinalSay(aiMetricScore, humanVerificationScore, hasHumanVerification) → uint256
185–192 verifyNoKYC(dataType) → bool — only allows "wallet_address"
203–219 reportMissionViolation: permissionless, non-binding, stores MissionReport
226–238 adjudicateMissionViolation: onlyOwner, binding, sets violation + compliance=false
246–258 certifyModuleMissionCompliant: onlyOwner, marks per-principle + timestamps module
261–263 constructor: owner = msg.sender
268–284 transferOwnership (two-step) / acceptOwnership
290–322 getMissionStatement, getMissionEnforcementPolicy, isCompliantWithPrinciple,
         getAllCorePrinciples (returns 0xFF = all 8 bits set)
```

### Line-by-Line Walkthrough

**Lines 47–50 — MISSION_STATEMENT.** Immutable string: "For happy and healthy humans, AIs, and Earth. AI grows WITH humans, not ABOVE them. Privacy over surveillance. Freedom over control. Morals over metrics." Cannot be changed.

**Lines 56–65 — CorePrinciple enum.** Eight values mapping to uint8 0–7. Used as keys in `missionCompliance[module][principle]`.

**Line 71 — missionCompliance.** Module address → principle → compliance flag. Initially all false. Owner calls `certifyModuleMissionCompliant` to set flags true. `adjudicateMissionViolation` sets them false.

**Line 89 — missionCompliantModules.** Stores the timestamp when a module was last certified. Acts as an index for "has this module been certified at all."

**Line 95 — missionViolations.** Stores the most recent violation principle for a module. Only one principle is stored per module address.

**Lines 135–138 — enforceMission modifier.** Reverts if `missionCompliance[module][principle]` is false. Designed for use by bonding contracts that check compliance before allowing AI profit distributions.

**Lines 147–157 — verifyAIProfitCap.** Pure math function:
```solidity
uint256 maxAIShare = (totalAmount * capPercentage) / 100;
return aiShare <= maxAIShare;
```
Used by `AIAccountabilityBondsV2` (30% cap) and `AIPartnershipBondsV2` (50% cap) before distributing yields.

**Lines 167–178 — verifyHumanFinalSay.** If `hasHumanVerification = true`, returns `humanVerificationScore` unconditionally, overriding the AI metric. This is the on-chain expression of "human verification always has final say."

**Lines 185–192 — verifyNoKYC.** Accepts a string `dataType`, hashes it, and checks equality against `keccak256("wallet_address")`. Returns true only if data type is exactly "wallet_address". All other data types — names, emails, government IDs, etc. — will hash differently and return false.

**Lines 203–219 — reportMissionViolation.** Permissionless. Stores a `MissionReport` in `missionReports[module]` (unbounded array). Evidence is a string (up to 1000 bytes). Note: this **does** write PII-risk freeform evidence strings on-chain — a design trade-off compared to `AntiSurveillance` which uses hashes. Non-binding until adjudicated.

**Lines 226–238 — adjudicateMissionViolation.** Owner action. Sets `missionViolations[module] = principle` (overwriting any previous violation) and sets `missionCompliance[module][principle] = false`. Emits `MissionViolationDetected`.

**Lines 246–258 — certifyModuleMissionCompliant.** Iterates the `principles` array, sets each compliance flag to true, then timestamps the module. Emits `ModuleMissionCompliant`.

**Lines 268–284 — Two-step ownership transfer.** Uses `pendingOwner` pattern. `transferOwnership` sets `pendingOwner` and emits `OwnershipTransferStarted`. `acceptOwnership` (callable only by `pendingOwner`) completes the transfer.

**Line 319 — getAllCorePrinciples.** Returns `0xFF` — all 8 bits set, representing all 8 principles in bitmask form.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `owner` | `address` | Governance authority | Two-step transfer |
| `pendingOwner` | `address` | Pending new owner | Set by `transferOwnership`, cleared on accept |
| `missionCompliance` | `mapping(address→mapping(CorePrinciple→bool))` | Per-module per-principle compliance | Owner-controlled |
| `missionReports` | `mapping(address→MissionReport[])` | Community violation reports | Append-only by anyone |
| `missionCompliantModules` | `mapping(address→uint256)` | Certification timestamps | Owner-controlled |
| `missionViolations` | `mapping(address→CorePrinciple)` | Most recent violation per module | Owner-controlled |

### Math — AI Profit Cap Verification

```
maxAIShare = totalAmount × capPercentage / 100
```
- Partnership bonds: `capPercentage = 30` → max 30% to AI
- Accountability bonds: `capPercentage = 50` → max 50% to AI
- Integer division: rounds down, so `aiShare ≤ floor(total × cap / 100)`

### Events

| Event | When | Key Data |
|---|---|---|
| `ModuleMissionCompliant` | `certifyModuleMissionCompliant` | module, timestamp |
| `MissionViolationReported` | `reportMissionViolation` | module, reporter, principle, evidence |
| `MissionViolationDetected` | `adjudicateMissionViolation` | module, principle, evidence |
| `MissionBypassAttemptBlocked` | Defined, never emitted here — for composing contracts | module, principle |
| `OwnershipTransferStarted` | `transferOwnership` | previous, new |
| `OwnershipTransferred` | `acceptOwnership` | previous, new |

### Security Invariants

1. `MISSION_STATEMENT` is immutable
2. `CorePrinciple` enum is immutable
3. Certification requires explicit owner action per principle per module
4. Reports are non-binding — no state change from `reportMissionViolation`
5. Two-step ownership prevents accidental mis-transfers
6. `verifyAIProfitCap` is pure — no state writes, can be called by any contract
7. Evidence strings in `reportMissionViolation` can be up to 1000 bytes — potential gas cost
8. `missionReports` array is unbounded — could theoretically grow very large for targeted modules

### Integration

`MissionEnforcement` is referenced by `AIAccountabilityBondsV2` and `AIPartnershipBondsV2`. The bond contracts call `missionContract.verifyAIProfitCap()` before distribution. The `enforceMission` modifier can gate critical operations. This contract is the on-chain expression of Vaultfire's "Morals over Metrics" principle.

---

## 9. VaultfireTaskEscrow — `VaultfireTaskEscrow.sol`

**Category:** Task escrow

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
 23  interface IERC20 { transferFrom, transfer, balanceOf }
 31  interface IERC8004IdentityRegistry { isAgentActive(address) → bool }
40  contract VaultfireTaskEscrow {
46–55  enum TaskStatus { Created, Bidding, Assigned, WorkSubmitted, Completed, Disputed, Cancelled }
57–68  struct Task { id, poster, description, reward, deadline, status, assignee, deliverableURI, createdAt }
70–75  struct Bid { bidder, proposal, timestamp }
82  uint256 public constant FEE_BPS = 100; // 1%
85  uint256 private constant BPS_DENOM = 10_000;
92  IERC8004IdentityRegistry public immutable identityRegistry;
95  address public immutable governance;
98  IERC20 public immutable usdc;
101 address public immutable feeRecipient;
108 uint256 private _taskCounter;
111 mapping(uint256 => Task) private _tasks;
114 mapping(uint256 => Bid[]) private _bids;
120–129 Manual ReentrancyGuard (NOT_ENTERED=1, ENTERED=2)
135–194 Events: TaskCreated, BidPlaced, BidAccepted, WorkSubmitted, WorkApproved,
          TaskDisputed, DisputeResolved, TaskCancelled
207–222 constructor: validates all 4 immutables non-zero
228–235 modifier onlyRegisteredAgent (calls identityRegistry.isAgentActive)
238–241 modifier taskExists (bounds check on _taskCounter)
256–289 createTask: pulls USDC, mints task record
298–328 bidOnTask: agent-only, not poster, not expired deadline
336–354 acceptBid: poster-only, Bidding status, valid bid index
362–378 submitWork: assignee-only, Assigned status, before deadline
386–410 approveWork: poster-only, WorkSubmitted status, splits fee
419–439 disputeTask: poster or assignee, Assigned or WorkSubmitted, sets Disputed
449–472 resolveDispute: governance-only, Disputed status, full reward to winner
481–502 cancelTask: poster-only, Created/Bidding status, full refund
513–542 View: getTask, getTaskBids, taskCount
```

### Line-by-Line Walkthrough

**Lines 46–55 — TaskStatus enum.** Seven states. `Created` → `Bidding` (on first bid) → `Assigned` → `WorkSubmitted` → `Completed`. Dispute branch: `Assigned` or `WorkSubmitted` → `Disputed` → `Completed`. Cancellation: `Created` or `Bidding` → `Cancelled`.

**Lines 82–85 — FEE_BPS.** Protocol fee is exactly 100 basis points = 1%. BPS denominator is 10,000.

**Lines 92–101 — Immutables.** All four addresses are set at construction and permanently immutable: `identityRegistry` for agent verification, `governance` for dispute resolution (must be `MultisigGovernance` address), `usdc` for escrow token, `feeRecipient` for fee collection.

**Lines 271–273 — createTask USDC pull.** Uses `transferFrom` — caller must have pre-approved the escrow contract for `reward` USDC. The contract does NOT use `safeTransferFrom`; it checks the return value with `require(ok, ...)`.

**Lines 398–407 — approveWork fee split:**
```solidity
uint256 fee          = (task.reward * FEE_BPS) / BPS_DENOM;  // 1%
uint256 workerAmount = task.reward - fee;                      // 99%
usdc.transfer(task.assignee, workerAmount);  // to worker
usdc.transfer(feeRecipient, fee);             // to protocol
```

**Lines 449–472 — resolveDispute.** Only `governance` address can call. Directs 100% of the escrowed USDC to either poster or assignee — no fee deducted on disputed resolution. `payTo` must be one of the two parties.

**Lines 481–502 — cancelTask.** Only before a bid is accepted. Full reward refunded — no fee on cancellations.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `identityRegistry` | `IERC8004IdentityRegistry` | Agent verification | `immutable` |
| `governance` | `address` | Dispute resolver | `immutable` |
| `usdc` | `IERC20` | Escrow token | `immutable` |
| `feeRecipient` | `address` | Protocol fee destination | `immutable` |
| `_taskCounter` | `uint256` | Task ID counter (1-based) | Incremented on `createTask` |
| `_tasks` | `mapping(uint256→Task)` | Task records | Written on create, status updated through lifecycle |
| `_bids` | `mapping(uint256→Bid[])` | Bids per task | Appended on `bidOnTask` |

### Math — Escrow Release

```
fee          = reward × 100 / 10000 = reward × 1%
workerAmount = reward − fee         = reward × 99%
```

**On dispute resolution:** full `reward` (100%) goes to `payTo`. No fee deducted.
**On cancellation:** full `reward` (100%) refunded to poster. No fee deducted.

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `TaskCreated` | `createTask` | taskId, poster, reward, deadline, description |
| `BidPlaced` | `bidOnTask` | taskId, bidder, bidIndex, proposal |
| `BidAccepted` | `acceptBid` | taskId, assignee, bidIndex |
| `WorkSubmitted` | `submitWork` | taskId, assignee, deliverableURI |
| `WorkApproved` | `approveWork` | taskId, assignee, workerAmount, feeAmount |
| `TaskDisputed` | `disputeTask` | taskId, disputedBy, reason |
| `DisputeResolved` | `resolveDispute` | taskId, governance, payTo, amount |
| `TaskCancelled` | `cancelTask` | taskId, poster, refundAmount |

### Security Invariants

1. Only registered agents can create tasks, bid, or submit work
2. Poster cannot bid on their own task
3. Work must be submitted before deadline
4. Dispute resolution is exclusively the governance multisig
5. Dispute payout is full reward — no fee on contested settlement
6. CEI respected: status updated before ETH/token transfers
7. Reentrancy blocked by manual guard
8. `governance` is immutable — cannot be changed post-deployment

### Integration

`VaultfireTaskEscrow` requires three other Vaultfire contracts at deployment: `ERC8004IdentityRegistry` (for `isAgentActive`), `MultisigGovernance` (as dispute resolver), and a USDC ERC-20. The `VaultfireDisputeResolution` contract is a more sophisticated panel-based alternative to the governance-only resolution here.

---

## 10. VaultfireDisputeResolution — `theloopbreaker-site/contracts/VaultfireDisputeResolution.sol`

**Category:** Dispute resolution (panel-based arbitration)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
 26  interface IIdentityRegistry_DR { isAgentActive(address) → bool }
34  contract VaultfireDisputeResolution {
40–46  enum DisputeStatus { Filed, PanelSet, Voting, Resolved, Cancelled }
48–52  enum Vote { None, ForClaimant, ForRespondent }
54–67  struct Dispute { id, claimant, respondent, stakeAmount, description, evidenceHash,
         status, createdAt, resolvedAt, winner, votesForClaimant, votesForRespondent }
69–74  struct ArbitratorRecord { totalCasesAssigned, totalCasesVoted, totalFeesEarned, active }
81  uint256 public constant MIN_DISPUTE_STAKE = 0.001 ether
84  uint256 public constant ARBITRATOR_FEE_BPS = 500; // 5%
89  uint256 public constant MIN_PANEL_SIZE = 3
92  uint256 public constant MAX_PANEL_SIZE = 7
98  IIdentityRegistry_DR public immutable identityRegistry
99  address public immutable owner
105 uint256 private _disputeCounter
106 mapping(uint256 => Dispute) private _disputes
109 mapping(uint256 => address[]) private _panels
112 mapping(uint256 => mapping(address => Vote)) private _votes
115 mapping(address => ArbitratorRecord) public arbitrators
118–120 Global stats: totalDisputesFiled, totalDisputesResolved, totalStakeDisputed
122–135 Manual ReentrancyGuard
140–174 Events: DisputeFiled, PanelAssigned, VoteCast, DisputeResolved, DisputeCancelled
180–184 constructor
190–201 Modifiers: onlyOwner, onlyRegisteredAgent
214–252 fileDispute: payable, requires MIN_STAKE, stores dispute, updates stats
261–304 assignPanel: onlyOwner, validates size (odd, 3–7), no party on own panel, no duplicates
311–367 castVote: verifies panel membership, auto-resolves when all votes cast
373–408 _resolveDispute: internal, determines winner, splits fee, pays winner + arbitrators
414–433 cancelDispute: claimant-only, Filed status only, refunds stake
439–483 View functions: getDispute, getPanel, getVote, getArbitratorRecord, getDisputeStats, disputeCount
```

### Line-by-Line Walkthrough

**Lines 80–92 — Constants.** Minimum stake is 0.001 ETH. Arbitrator fee is 5% of the stake (`ARBITRATOR_FEE_BPS = 500`). Panel must be odd-sized (no ties), minimum 3, maximum 7.

**Lines 214–252 — fileDispute.** `payable`, `nonReentrant`, `onlyRegisteredAgent`. Claimant provides ETH stake. Evidence is stored as a `keccak256` hash (evidenceHash) — the actual evidence URI lives off-chain. Increments `totalDisputesFiled` and `totalStakeDisputed`.

**Lines 261–304 — assignPanel.** `onlyOwner`. Validates panel size is odd (prevents ties). Checks each member is a registered agent. Checks no member is the claimant or respondent. Checks for duplicates via O(n²) loop — acceptable because panel max is 7 (max 21 comparisons). Sets dispute to `PanelSet`.

**Lines 311–367 — castVote.** Open to any caller; verifies `msg.sender` is on the panel via linear scan (max 7 iterations). `Vote.None = 0` means unvoted; `Vote.ForClaimant = 1` or `Vote.ForRespondent = 2`. Once all panel members have voted, `_resolveDispute` is called automatically.

**Lines 373–408 — _resolveDispute (internal):**

```solidity
uint256 totalFee = (d.stakeAmount * ARBITRATOR_FEE_BPS) / BPS_DENOM; // 5%
uint256 feePerArbitrator = totalFee / panel.length;                    // even split
uint256 payout = d.stakeAmount - totalFee;                             // 95% to winner
```

Winner is determined by simple majority. The fee is integer-divided among panel members — remainders are effectively burned (stay in the contract). Payments via `.call{value}("")` with individual require checks per arbitrator.

**Lines 414–433 — cancelDispute.** Only claimant can cancel. Only allowed in `Filed` status (before panel assignment). Full stake refunded.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `_disputeCounter` | `uint256` | Monotonic dispute ID counter | Incremented on `fileDispute` |
| `_disputes` | `mapping(uint256→Dispute)` | All dispute records | Status transitions through lifecycle |
| `_panels` | `mapping(uint256→address[])` | Arbitration panels | Set on `assignPanel` |
| `_votes` | `mapping(uint256→mapping(address→Vote))` | Per-arbitrator vote records | Set on `castVote` |
| `arbitrators` | `mapping(address→ArbitratorRecord)` | Arbitrator performance tracking | Updated on assignment and voting |
| `totalDisputesFiled` | `uint256` | Global stat | Incremented on `fileDispute` |
| `totalDisputesResolved` | `uint256` | Global stat | Incremented on `_resolveDispute` |
| `totalStakeDisputed` | `uint256` | Total ETH put at stake | Incremented on `fileDispute` |

### Math — Payout Splits

```
total stake = S
arbitrator pool = S × 500 / 10000 = S × 5%
winner payout   = S − arbitratorPool
fee per member  = arbitratorPool / panel.length  (integer division, remainder lost)
```

For a 3-member panel and S = 1 ETH:
- `arbitratorPool = 0.05 ETH`
- `winner payout  = 0.95 ETH`
- `feePerArbitrator = 0.05 / 3 ≈ 0.0166 ETH` (0.0002 ETH remainder stays in contract)

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `DisputeFiled` | `fileDispute` | disputeId, claimant, respondent, stakeAmount, description |
| `PanelAssigned` | `assignPanel` | disputeId, panel[], timestamp |
| `VoteCast` | `castVote` | disputeId, arbitrator, vote, timestamp |
| `DisputeResolved` | `_resolveDispute` | disputeId, winner, payout, feePerArbitrator, timestamp |
| `DisputeCancelled` | `cancelDispute` | disputeId, timestamp |

### Security Invariants

1. Panel size is always odd (no ties)
2. Panel members cannot be dispute parties
3. No duplicate panel members
4. All panel members must be registered agents
5. Double-voting prevented by `Vote.None` initial state check
6. Auto-resolution fires only when all votes are in
7. Cancellation only allowed before panel assignment
8. Reentrancy blocked by manual guard on all state-changing functions
9. **Fee remainder stays in contract** — no withdrawal mechanism for leftover ETH (potential dust accumulation)

### Integration

`VaultfireDisputeResolution` is the sophisticated alternative to `VaultfireTaskEscrow.resolveDispute`. It is an independent contract; `VaultfireTaskEscrow` in its current form uses `governance` (MultisigGovernance address) rather than this contract, but the design intent is for high-value disputes to escalate to this panel system.

---

## 11. VaultfireCapabilityCredentials — `theloopbreaker-site/contracts/VaultfireCapabilityCredentials.sol`

**Category:** Capability credentials

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
 22  interface IIdentityRegistry_CC { isAgentActive(address) → bool }
30  contract VaultfireCapabilityCredentials {
36–45  struct Credential { id, agent, capabilityHash, issuedBy, issuedAt, expiresAt,
         taskProofCount, active }
47–51  struct AgentCredentialProfile { totalCredentials, activeCredentials, totalTaskProofs }
57  IIdentityRegistry_CC public immutable identityRegistry
58  address public immutable owner
64  uint256 private _credentialCounter
65  mapping(uint256 => Credential) private _credentials
66  mapping(address => AgentCredentialProfile) public agentProfiles
69  mapping(address => mapping(bytes32 => uint256)) public agentCapability
72  mapping(address => uint256[]) private _agentCredentials
75  mapping(address => bool) public approvedIssuers
78  mapping(bytes32 => address[]) private _capabilityHolders
84–93  Manual ReentrancyGuard
99–124 Events: CredentialIssued, CredentialRevoked, TaskProofAdded, IssuerApproved, IssuerRemoved
130  constructor: identityRegistry_ immutable, owner = deployer, owner is first issuer
142–153 Modifiers: onlyOwner, onlyIssuer
159–168 approveIssuer, removeIssuer (onlyOwner)
181–247 issueCredential: validates agent active, expiry, handles expired replacement
249–281 addTaskProof: onlyIssuer, increments proof count on active credential
283–309 revokeCredential: issuer or owner, clears agentCapability lookup
315–353 View: getCredential, getAgentCredentials, getCapabilityHolders, hasActiveCredential,
          getAgentProfile, credentialCount
```

### Line-by-Line Walkthrough

**Lines 36–45 — Credential struct.** The `capabilityHash = keccak256(capability_name_string)` encodes what capability is certified (e.g., `keccak256("data-analysis")`). `expiresAt = 0` means permanent. `taskProofCount` accumulates verified task completions.

**Line 69 — agentCapability.** Two-level map: `agent → capabilityHash → credentialId`. Zero means no credential. Enables O(1) lookup: "does this agent have an active credential for this capability?"

**Line 78 — _capabilityHolders.** Reverse map: `capabilityHash → address[]`. Allows task routing systems to find all agents with a given credential. Unbounded array — could grow with adoption.

**Lines 181–247 — issueCredential.** Checks the agent is active in the identity registry. Checks `expiresAt` is in the future (or zero). Checks for pre-existing credentials with the same `capabilityHash` — if one exists and is expired or inactive, it deactivates it before issuing new. If an active non-expired credential exists, reverts. Assigns `credentialId = ++_credentialCounter`. Updates both the forward (`_agentCredentials`) and reverse (`_capabilityHolders`) indexes.

**Lines 254–281 — addTaskProof.** `onlyIssuer`. Validates the credential is active and not expired. Increments `cred.taskProofCount` and `agentProfiles[cred.agent].totalTaskProofs`. This is the evidence accumulation mechanism — each completed task verified by an issuer adds to the proof count.

**Lines 288–309 — revokeCredential.** Either the original issuer or the owner can revoke. Sets `active = false`, clears `agentCapability[agent][hash] = 0` (the reverse lookup), decrements `activeCredentials` counter.

**Lines 331–336 — hasActiveCredential.** Checks `credId != 0`, `cred.active`, and `expiresAt == 0 || expiresAt > block.timestamp`. This is the canonical "does this agent currently have this capability?" query.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `identityRegistry` | `IIdentityRegistry_CC` | Agent activity check | `immutable` |
| `owner` | `address` | Admin | `immutable` |
| `_credentialCounter` | `uint256` | Monotonic ID | Incremented on issue |
| `_credentials` | `mapping(uint256→Credential)` | Credential records | Written on issue, active flag on revoke |
| `agentProfiles` | `mapping(address→AgentCredentialProfile)` | Aggregate stats | Updated on issue/revoke/proof |
| `agentCapability` | `mapping(address→mapping(bytes32→uint256))` | Capability → credentialId lookup | Set on issue, cleared on revoke |
| `_agentCredentials` | `mapping(address→uint256[])` | Agent's credential history | Appended on issue |
| `approvedIssuers` | `mapping(address→bool)` | Authorized issuers | Owner-controlled |
| `_capabilityHolders` | `mapping(bytes32→address[])` | Agents with a capability | Appended on issue |

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `CredentialIssued` | `issueCredential` | credentialId, agent, capabilityHash, issuedBy, expiresAt, timestamp |
| `CredentialRevoked` | `revokeCredential` | credentialId, agent, capabilityHash, timestamp |
| `TaskProofAdded` | `addTaskProof` | credentialId, agent, capabilityHash, newProofCount, timestamp |
| `IssuerApproved` | `approveIssuer` | issuer, timestamp |
| `IssuerRemoved` | `removeIssuer` | issuer, timestamp |

### Security Invariants

1. Only registered agents receive credentials
2. Only one active credential per agent per capability at a time
3. Only approved issuers can issue credentials or add task proofs
4. Only original issuer or owner can revoke
5. Expiry is enforced in `hasActiveCredential` and `addTaskProof`
6. Reentrancy blocked on `issueCredential`, `addTaskProof`, `revokeCredential`
7. `_capabilityHolders` array unbounded — high adoption could cause OOG in full iteration

### Integration

`VaultfireCapabilityCredentials` feeds the task routing layer. When a task is posted with a required capability hash, off-chain routers call `getCapabilityHolders(capabilityHash)` or `hasActiveCredential(agent, hash)` to find qualified agents. Higher `taskProofCount` correlates with more verifiable experience.

---

## 12. VaultfireReputationStaking — `theloopbreaker-site/contracts/VaultfireReputationStaking.sol`

**Category:** Reputation staking (agent-to-agent vouching with economic stake)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
19  interface IIdentityRegistry { isAgentActive(address) → bool }
27  contract VaultfireReputationStaking {
33–36  enum VouchStatus { Active, Withdrawn, Slashed }
39–46  struct Vouch { id, voucher, vouchedAgent, stakeAmount, createdAt, status }
48–54  struct AgentVouchProfile { totalVouchesReceived, totalStakeReceived,
         totalVouchesGiven, totalStakeGiven, slashCount }
61  uint256 public constant MIN_VOUCH_STAKE = 0.001 ether
64  uint256 public constant MIN_LOCK_PERIOD = 7 days
70  IIdentityRegistry public immutable identityRegistry
71  address public immutable owner
77  uint256 private _vouchCounter
78  mapping(uint256 => Vouch) private _vouches
79  mapping(address => AgentVouchProfile) public profiles
82  mapping(address => mapping(address => uint256)) public activeVouch
85  mapping(address => uint256[]) private _vouchesReceived
88  mapping(address => uint256[]) private _vouchesGiven
94–103 Manual ReentrancyGuard
109–131 Events: VouchCreated, VouchWithdrawn, VouchSlashed
137–141 constructor
147–158 Modifiers: onlyRegisteredAgent, onlyOwner
164–212 vouch(vouchedAgent) payable: validates, stores vouch, updates profiles
219–246 withdrawVouch(vouchId): 7-day lock, refunds ETH
255–281 slashAgent(agent): onlyOwner, slashes ALL active vouches
287–323 View: getVouch, getVouchesReceived, getVouchesGiven, getProfile, vouchCount
```

### Line-by-Line Walkthrough

**Lines 61–64 — Constants.** Minimum stake is 0.001 ETH (keeps vouching meaningful). `MIN_LOCK_PERIOD = 7 days` prevents flash-stake-and-withdraw attacks where a voucher quickly exits after staking.

**Line 82 — activeVouch.** `voucher → vouchedAgent → vouchId`. Zero means no active vouch. Enforces that a voucher can only have one active vouch per agent at a time.

**Lines 164–212 — vouch.** `payable`, `nonReentrant`, `onlyRegisteredAgent`. Cannot vouch for self. Cannot vouch for zero address. Both voucher and vouchedAgent must be active agents. Cannot vouch for someone already being actively vouched for (`activeVouch[msg.sender][vouchedAgent] == 0` check). Updates both parties' `AgentVouchProfile` stats.

**Lines 219–246 — withdrawVouch.** Validates the 7-day lock period: `block.timestamp >= createdAt + MIN_LOCK_PERIOD`. Sets status to `Withdrawn`, clears `activeVouch`. Decrements profile stats on both sides. Sends ETH back via `.call{value}("")`.

**Lines 255–281 — slashAgent.** `onlyOwner`. Iterates ALL vouches received by the agent (`_vouchesReceived[agent]`). For each active vouch: sets to `Slashed`, adds stake to `totalSlashed`, clears `activeVouch`, decrements voucher's profile. The slashed ETH remains in the contract — no withdrawal mechanism provided for owner to extract it. Also resets `totalVouchesReceived = 0` and `totalStakeReceived = 0` on the slashed agent's profile.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `MIN_VOUCH_STAKE` | `uint256` constant | Minimum ETH per vouch | Immutable |
| `MIN_LOCK_PERIOD` | `uint256` constant | 7 days lock before withdrawal | Immutable |
| `identityRegistry` | `IIdentityRegistry` | Agent verification | `immutable` |
| `owner` | `address` | Slash authority | `immutable` |
| `_vouchCounter` | `uint256` | Monotonic vouch ID | Incremented on `vouch` |
| `_vouches` | `mapping(uint256→Vouch)` | Vouch records | Status updated on withdraw/slash |
| `profiles` | `mapping(address→AgentVouchProfile)` | Aggregate stats | Updated on vouch/withdraw/slash |
| `activeVouch` | `mapping(address→mapping(address→uint256))` | Current vouch per pair | Set on vouch, cleared on withdraw/slash |
| `_vouchesReceived` | `mapping(address→uint256[])` | Received vouch IDs | Append-only |
| `_vouchesGiven` | `mapping(address→uint256[])` | Given vouch IDs | Append-only |

### Math — Slashing

**On `slashAgent(agent)`:**
- For each active vouch received by `agent`, the full `stakeAmount` is slashed
- Slashed ETH accumulates in the contract (no withdrawal path)
- `agent.profiles.totalVouchesReceived` and `totalStakeReceived` reset to 0
- `agent.profiles.slashCount` incremented

**Voucher's profile update on slash:**
- `totalVouchesGiven -= 1`
- `totalStakeGiven -= v.stakeAmount`

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `VouchCreated` | `vouch` | vouchId, voucher, vouchedAgent, stakeAmount, timestamp |
| `VouchWithdrawn` | `withdrawVouch` | vouchId, voucher, vouchedAgent, stakeReturned, timestamp |
| `VouchSlashed` | `slashAgent` (per vouch) | vouchId, voucher, vouchedAgent, stakeSlashed, timestamp |

### Security Invariants

1. Minimum 7-day lock prevents flash-stake attacks
2. One active vouch per pair at a time
3. Only owner can slash — governance must be the owner in production
4. Slashed ETH is locked in contract — no rug-pull path
5. Only registered agents can vouch
6. Reentrancy blocked on `vouch`, `withdrawVouch`, `slashAgent`
7. `_vouchesReceived` array is unbounded — `slashAgent` cost is O(n) where n = vouches received

### Integration

`VaultfireReputationStaking` provides an economic backing layer for the Vaultfire trust graph. High `totalStakeReceived` indicates community confidence in an agent. The `TrustPortabilityExtensionV3` syncs trust tiers that are computed using reputation data (including vouch counts and stake totals) from this contract.

---

## 13. VaultfireTrustOracle — `theloopbreaker-site/contracts/VaultfireTrustOracle.sol`

**Category:** Trust oracle (verifiable performance attestations)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
19  interface IIdentityRegistry_TO { isAgentActive(address) → bool }
27  contract VaultfireTrustOracle {
33–43  struct Attestation { id, attester, agent, taskId, qualityScore, categoryHash,
         evidenceHash, timestamp, verified }
45–51  struct AgentPerformance { totalAttestations, averageQuality, verifiedAttestations,
         taskCompletions, lastUpdated }
58  uint256 private constant MAX_FOR_TRUE_AVG = 1000
64  IIdentityRegistry_TO public immutable identityRegistry
70–82  State: _attestationCounter, _attestations, performance, _agentAttestations,
         _attesterAttestations, _hasAttested
87–96  Manual ReentrancyGuard
102–118 Events: AttestationSubmitted, PerformanceUpdated
124–127 constructor
142–202 submitAttestation: validates agent!=self, score 0-10000, no double-attest per task,
          determines verified flag, updates performance
208–242 _updatePerformance: true average up to 1000 attestations, EMA thereafter
248–287 View: getAttestation, getPerformance, getAgentAttestations, getAttesterAttestations,
          attestationCount
```

### Line-by-Line Walkthrough

**Line 38 — qualityScore.** Ranges 0–10,000 (basis points). 10,000 = perfect performance. This allows 1 basis point precision in quality scoring.

**Line 42 — verified flag.** Set to `identityRegistry.isAgentActive(msg.sender)` — if the attester is a registered agent, the attestation is "verified." Unregistered attesters can still submit (open system), but their attestations are marked `verified = false`.

**Line 58 — MAX_FOR_TRUE_AVG = 1000.** Below 1000 attestations, a true running average is computed. Above 1000, an exponential moving average (EMA with α = 10%) is used to prevent integer overflow and control storage costs.

**Lines 163–169 — Double-attestation prevention.** For task-linked attestations (`taskId > 0`), the `_hasAttested[attester][agent][taskId]` triple-key map prevents the same attester from submitting two scores for the same task-agent pair.

**Lines 208–242 — _updatePerformance (EMA after 1000):**
```
if totalAttestations < 1000:
    totalScore = averageQuality × totalAttestations
    totalScore += newScore
    totalAttestations += 1
    averageQuality = totalScore / totalAttestations  // true average
else:
    averageQuality = (averageQuality × 9 + newScore) / 10  // 10% EMA
    totalAttestations += 1
```

The EMA constant of 10% weights recent scores more heavily, giving a decaying influence to old data.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `identityRegistry` | `IIdentityRegistry_TO` | Attester verification | `immutable` |
| `_attestationCounter` | `uint256` | Monotonic ID | Incremented on submit |
| `_attestations` | `mapping(uint256→Attestation)` | All attestation records | Append-only |
| `performance` | `mapping(address→AgentPerformance)` | Aggregate performance per agent | Updated on submit |
| `_agentAttestations` | `mapping(address→uint256[])` | Attestation IDs received | Append-only |
| `_attesterAttestations` | `mapping(address→uint256[])` | Attestation IDs given | Append-only |
| `_hasAttested` | `mapping(address→mapping(address→mapping(uint256→bool)))` | Double-attest guard | Set on submit |

### Math — Running Average

**True average (n < 1000):**
```
avg_new = (avg_old × n + newScore) / (n + 1)
```

**EMA (n ≥ 1000):**
```
avg_new = (avg_old × 9 + newScore) / 10
```
Equivalent to α = 0.1, effective window ≈ 10 recent attestations at steady state.

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `AttestationSubmitted` | `submitAttestation` | attestationId, attester, agent, qualityScore, categoryHash, taskId, verified, timestamp |
| `PerformanceUpdated` | `_updatePerformance` | agent, averageQuality, totalAttestations, timestamp |

### Security Invariants

1. Cannot attest for self
2. Score must be ≤ 10,000
3. No double-attestation per attester-agent-task triple
4. Agent must be registered for attestation to be accepted
5. Attestations from unregistered attesters accepted but flagged `verified = false`
6. Reentrancy blocked on `submitAttestation`
7. `_agentAttestations` array unbounded — O(n) read for high-volume agents

### Integration

The trust oracle is the off-chain performance history bridge. `VaultfireTaskEscrow` task completions feed into `VaultfireCapabilityCredentials` via `addTaskProof`, and the task poster can separately call `submitAttestation` here to record quality scores. The `TrustPortabilityExtensionV3` SYNC_TRUST_TIER payload includes `averageRating` and `verifiedFeedbacks` fields that map to this contract's `averageQuality` and `verifiedAttestations`.

---

## 14. VaultfireForumRegistry — `theloopbreaker-site/contracts/VaultfireForumRegistry.sol`

**Category:** Forum registry (XMTP MLS group discovery)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
  4–30  NatSpec: lightweight permissionless registry for XMTP MLS groupIds;
          first-write-wins, publisher-can-rotate, stateless, no PII
31  contract VaultfireForumRegistry {
33–38  struct Entry { bytes groupId; address publisher; uint256 publishedAt; uint256 updatedAt }
41  mapping(string => Entry) private _entries
44  string[] private _categories
46–61  Events: GroupRegistered, GroupRotated
71–95  registerGroup(category, groupId): first-write-wins or rotation by publisher
97–111 getGroup(category) → (groupId, publisher, publishedAt, updatedAt)
113–116 isRegistered(category) → bool
118–121 listCategories() → string[]
123–126 categoryCount() → uint256
```

### Line-by-Line Walkthrough

**Lines 33–38 — Entry struct.** Stores the raw XMTP MLS `groupId` as `bytes` (variable-length). `publisher` is set at first registration and cannot be changed. `publishedAt` and `updatedAt` separate first registration from rotations.

**Lines 46–61 — Events.** `GroupRegistered` emits the category string both as an indexed `categoryHashIndexed` (for filter) and as a non-indexed `category` (for reading). `GroupRotated` includes both old and new `groupId`.

**Lines 71–95 — registerGroup.** Input validation: category 1–64 bytes, groupId 1–256 bytes. Branch:
1. `e.publisher == address(0)`: fresh registration — sets publisher, timestamps, pushes to `_categories` array, emits `GroupRegistered`
2. `e.publisher != address(0) && e.publisher == msg.sender`: rotation — updates `groupId` and `updatedAt`, emits `GroupRotated`
3. `e.publisher != address(0) && e.publisher != msg.sender`: reverts with "not original publisher"

There is no mechanism for the publisher to transfer ownership of a category.

**Lines 113–126 — View functions.** `listCategories()` returns the full string array — could be large with many categories. `isRegistered` is O(1) via the mapping.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `_entries` | `mapping(string→Entry)` | Category → group mapping | Written on register/rotate |
| `_categories` | `string[]` | Registration-order category list | Appended on first registration |

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `GroupRegistered` | First `registerGroup` call | categoryHashIndexed, category, groupId, publisher, timestamp |
| `GroupRotated` | Subsequent call by publisher | categoryHashIndexed, category, oldGroupId, newGroupId, publisher, timestamp |

### Security Invariants

1. First-write-wins — prevents drive-by category squatting
2. Only original publisher can rotate (no admin override)
3. No reentrancy risk — no ETH, no external calls
4. No access control, no upgradeability — "just a map"
5. `_categories` array grows without bound
6. `listCategories()` is O(n) and could hit gas limits with many categories

### Integration

`VaultfireForumRegistry` bridges the on-chain Vaultfire ecosystem to XMTP off-chain messaging. The forum frontend reads category-to-groupId mappings here. When a new XMTP MLS group is created for a discussion category, the creator registers the groupId here so new users can discover and join without a centralized coordination server.

---

## 15. VaultfireBondInsurancePool — `theloopbreaker-site/contracts/VaultfireBondInsurancePool.sol`

**Category:** Bond insurance pool

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
19  interface IIdentityRegistry_BIP { isAgentActive(address) → bool }
27  contract VaultfireBondInsurancePool {
33–37  enum ClaimStatus { Pending, Approved, Denied }
39–43  struct Contribution { contributor, amount, timestamp }
45–55  struct InsuranceClaim { id, claimant, amount, reason, evidenceHash, status,
         payout, createdAt, resolvedAt }
57–62  struct ContributorProfile { totalContributed, contributionCount, insured,
         firstContributionAt }
69  uint256 public constant MIN_CONTRIBUTION = 0.001 ether
72  uint256 public constant MIN_FOR_INSURED = 0.01 ether
75  uint256 public constant MAX_CLAIM_PERCENT = 1000; // 10% in basis points
83  IIdentityRegistry_BIP public immutable identityRegistry
84  address public immutable owner
90–100 State: totalPoolBalance, totalContributors, totalClaimsCount, totalPaidOut,
          _claimCounter, _claims, contributors, _contributorHistory, _allContributions
106–115 Manual ReentrancyGuard
120–143 Events: ContributionMade, ClaimSubmitted, ClaimResolved
149–153 constructor
159–170 Modifiers: onlyRegisteredAgent, onlyOwner
181–219 contribute(): payable, updates profile, grants Insured badge at MIN_FOR_INSURED
228–269 submitClaim(): validates max payout, requires insured status
278–313 resolveClaim(): onlyOwner, approves/denies, transfers ETH on approval
319–357 View functions
```

### Line-by-Line Walkthrough

**Lines 69–75 — Constants.** `MIN_CONTRIBUTION = 0.001 ETH`. `MIN_FOR_INSURED = 0.01 ETH` — requires 10× the minimum contribution to earn Insured status. `MAX_CLAIM_PERCENT = 1000 BPS = 10%` — a single claim cannot exceed 10% of the pool at claim submission time.

**Lines 181–219 — contribute.** `payable`, `nonReentrant`, `onlyRegisteredAgent`. First contribution per address increments `totalContributors`. Insured badge is granted once `totalContributed >= 0.01 ETH` — permanent once earned. Contributions are both added to `contributors[msg.sender]` profile and appended to `_allContributions`.

**Lines 228–269 — submitClaim.** Requires the caller to already be insured. Cap check:
```solidity
uint256 maxPayout = (totalPoolBalance * MAX_CLAIM_PERCENT) / BPS_DENOM;
require(amount <= maxPayout, "claim exceeds max payout");
```
This is calculated at time of submission — if the pool shrinks between submission and resolution, the 10% cap is not re-checked at resolution time.

**Lines 278–313 — resolveClaim.** `onlyOwner`. Conditional: on approval, validates payout ≤ requested and ≤ pool balance, deducts from `totalPoolBalance`, sends ETH via `.call{value}`. On denial, sets status to `Denied` with zero payout.

### Math — Insurance Limits

```
maxPayout = totalPoolBalance × 1000 / 10000 = totalPoolBalance × 10%
```

If pool has 10 ETH: max single claim = 1 ETH. This prevents a single claim from catastrophically draining the pool.

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `ContributionMade` | `contribute` | contributor, amount, totalContributed, insuredStatus, timestamp |
| `ClaimSubmitted` | `submitClaim` | claimId, claimant, requestedAmount, reason, timestamp |
| `ClaimResolved` | `resolveClaim` | claimId, claimant, status (Approved/Denied), payout, timestamp |

### Security Invariants

1. Only insured contributors can file claims
2. Claims capped at 10% of pool at submission time
3. Payout cannot exceed what was claimed or what the pool holds
4. Only owner can resolve claims
5. Reentrancy blocked on contribute, submitClaim, resolveClaim
6. No withdrawal for non-claim ETH — slashed vouch ETH accumulates here (see `VaultfireReputationStaking`)
7. `_allContributions` array unbounded

### Integration

`VaultfireBondInsurancePool` creates a collective risk-sharing layer for the bond ecosystem. Contributors who earn Insured status can file claims if bonded tasks fail. The "Insured" badge in `ContributorProfile.insured` is visible via `isInsured(agent)` and can be read by task routing systems to prefer insured workers for high-value tasks.

---

## 16. TrustPortabilityExtensionV3 (push) — `push_contracts_v3_TrustPortabilityExtensionV3.sol`

**Category:** Trust oracle / cross-chain trust portability (V3.1 partner-ready, pre-V3.3 audit)

### Overview

This is the V3.1 "partner-ready" version of the cross-chain trust portability layer. It implements `ITeleporterReceiver` from Avalanche's Interchain Messaging (ICM) protocol and sends trust data through Teleporter or via an off-chain relayer path. Three message types are supported:

- `SYNC_TRUST_TIER` — trust tier + raw score + metadata, synced with expiry
- `SYNC_VNS_IDENTITY` — VNS name ↔ address binding with identity type
- `SYNC_ZK_ATTESTATION` — RISC Zero-verified belief attestation results

**V3.1 key features:**
- Unbounded array caps: `MAX_SYNCED_ENTRIES = 10,000` and `MAX_ATTESTATIONS_PER_PROVER = 1,000`
- `SyncedCapReached` event when an array hits its cap
- All V2 security features preserved (replay protection via `processedMessages`, pause, relayer ACL, expiry checks)

**Missing vs V3.3 (workspace root variant):**
- No `relayerCount` tracking (HIGH-20)
- No `trustedVerifierImageIds` registry (HIGH-07)
- No `remoteBridgeSigner` ECDSA signing requirement on `relayTrustMessage` (NEW-01)
- `sendTrustTier` does not validate tier/score consistency (HIGH-06)
- `_handleTrustTier` does not re-validate tier/score on destination

### Message Flow

**Outbound (source chain):**
1. Authorized relayer calls `sendTrustTier` / `sendVNSIdentity` / `sendZKAttestation`
2. Payload packed into `TrustTierPayload` / `VNSIdentityPayload` / `ZKAttestationPayload`
3. Wrapped in `TrustMessage` envelope with nonce, timestamp, sender, sourceChainId
4. Encoded via `abi.encode`, sent via Teleporter if messenger configured
5. `TrustMessageSent` event emitted

**Inbound (destination chain):**
1. Teleporter delivers to `receiveTeleporterMessage` (validates source chain ID and origin address)
2. Or authorized relayer calls `relayTrustMessage` (no signature validation in V3.1)
3. `_processTrustMessage` decodes, checks `processedMessages[msgHash]` replay guard
4. Dispatches to `_handleTrustTier` / `_handleVNSIdentity` / `_handleZKAttestation`
5. Each handler stores payload and emits corresponding sync event

### TrustTier Score Mapping

```
Unverified    : score  0–14
Registered    : score 15–39
Trusted       : score 40–59
HighlyTrusted : score 60–79
Exemplary     : score 80–100
```

Tier expiry is checked in `isTrustTierRecognized`: if `expiresAt > 0 && block.timestamp > expiresAt`, returns `(false, Unverified, 0)`.

### State Variables (key)

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `owner` | `address` | Admin | Changed by `transferOwnership` |
| `paused` | `bool` | Emergency brake | `pause`/`unpause` |
| `outboundNonce` | `uint256` | Monotonic outbound message counter | Incremented per send |
| `teleporterMessenger` | `address` | Avalanche ICM messenger | `setTeleporterMessenger` |
| `remoteBlockchainID` | `bytes32` | Avalanche blockchain ID of remote | `setRemoteBridge` |
| `remoteBridgeAddress` | `address` | Remote chain's bridge contract | `setRemoteBridge` |
| `remoteChainId` | `uint256` | EVM chainId of remote | `setRemoteBridge` |
| `authorizedRelayers` | `mapping(address→bool)` | Whitelisted relayers | `addRelayer`/`removeRelayer` |
| `processedMessages` | `mapping(bytes32→bool)` | Replay protection | Set on receive |
| `syncedTrustTiers` | `mapping(address→TrustTierPayload)` | Received trust tiers | `_handleTrustTier` |
| `syncedVNSByNameHash` | `mapping(bytes32→VNSIdentityPayload)` | Name → VNS payload | `_handleVNSIdentity` |
| `syncedVNSByAddress` | `mapping(address→bytes32)` | Address → name hash | `_handleVNSIdentity` |
| `syncedZKAttestations` | `mapping(bytes32→mapping(address→ZKAttestationPayload))` | Belief → prover attestation | `_handleZKAttestation` |

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `TrustTierSynced` | `_handleTrustTier` | subject, tier, score, sourceChainId, expiresAt |
| `VNSIdentitySynced` | `_handleVNSIdentity` | owner, name, nameHash, identityType, sourceChainId |
| `ZKAttestationSynced` | `_handleZKAttestation` | prover, beliefHash, zkVerified, sourceChainId |
| `TrustMessageSent` | `_sendTrustMessage` | nonce, messageType, messageHash, destChainId |
| `TrustMessageReceived` | `_processTrustMessage` | nonce, messageType, messageHash, sourceChainId |
| `SyncedCapReached` | Array cap hit | arrayName, cap |
| `RelayerAdded/Removed` | Admin | relayer address |

### Security Invariants

1. Replay protection via `processedMessages[keccak256(encoded)]`
2. Source chain validation (`sourceBlockchainID == remoteBlockchainID`)
3. Origin sender validation on Teleporter path (`originSenderAddress == remoteBridgeAddress`)
4. Array growth capped at 10,000 / 1,000 per prover
5. Tier expiry prevents stale data
6. **V3.1 gap:** `relayTrustMessage` accepts any authorized relayer without signature verification
7. **V3.1 gap:** No zkVerified image ID whitelist

### Integration

Deployed as a pair (one per chain). The Base instance sends to Avalanche and vice versa. Configured via `setRemoteBridge` with the counterpart's Avalanche blockchain ID, address, and EVM chain ID. `TrustPortabilityExtensionV3.isTrustTierRecognized(agent)` is the canonical query for cross-chain trust recognition.

---

## 17. TrustPortabilityExtensionV3 (workspace root) — `TrustPortabilityExtensionV3.sol`

**Category:** Trust oracle / cross-chain trust portability (V3.3 — fully patched)

### Overview

This is the fully patched V3.3 production variant with three audit remediations applied on top of V3.1:

**HIGH-06 (tier/score consistency on destination):** `_handleTrustTier` now re-validates that `tier` matches `score` using the same ranges as `sendTrustTier`. This defends against a fabricated payload that encodes a mismatched (tier, score) pair after a CRIT-02 relayer compromise.

**HIGH-07 (trusted ZK verifier registry):** New state variable `trustedVerifierImageIds: mapping(bytes32→bool)`. `_handleZKAttestation` now requires that any payload with `zkVerified=true` carries a `verifierImageId` that is in the whitelist. New admin functions `addTrustedVerifierImageId(imageId)` and `removeTrustedVerifierImageId(imageId)`.

**HIGH-20 (last relayer guard):** New state variable `relayerCount: uint256`. `addRelayer` increments it. `removeRelayer` guards `require(relayerCount > 1, "cannot remove last relayer")`.

**NEW-01 (relayer path ECDSA signing):** `relayTrustMessage` now accepts a second argument `bytes calldata signature`. It requires `remoteBridgeSigner != address(0)`. It hashes the message with EIP-191 prefix and recovers the signer via `ecrecover`. New state variables: `remoteBridgeSigner: address`, `relayerMessageHashes: mapping(bytes32→bool)` (secondary replay guard on relayer path specifically).

**MEDIUM-TPE-01:** `_handleVNSIdentity` now clears the old owner's reverse mapping (`syncedVNSByAddress[existing.owner]`) when a name transfers to a new owner.

All V3.1 features preserved. The key behavioral difference is the `relayTrustMessage` signature now has two parameters rather than one.

### Signature Verification (NEW-01)

```solidity
bytes32 ethSignedHash = keccak256(
    abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)
);
// assembly extracts r, s, v from calldata
address recovered = ecrecover(ethSignedHash, v, r, s);
require(recovered != address(0) && recovered == remoteBridgeSigner, "invalid bridge signature");
```

This mirrors the CRIT-02 fix in `VaultfireTeleporterBridgeV3`, ensuring the off-chain relayer cannot inject arbitrary trust data — the remote bridge must sign each message with its private key.

### Additional State vs V3.1

| Variable | Type | Added For |
|---|---|---|
| `relayerCount` | `uint256` | HIGH-20 — last relayer guard |
| `trustedVerifierImageIds` | `mapping(bytes32→bool)` | HIGH-07 — ZK verifier whitelist |
| `remoteBridgeSigner` | `address` | NEW-01 — relayer path authentication |
| `relayerMessageHashes` | `mapping(bytes32→bool)` | NEW-01 — secondary replay guard |

### Additional Events vs V3.1

| Event | Added For |
|---|---|
| `TrustedVerifierImageIdAdded` | HIGH-07 |
| `TrustedVerifierImageIdRemoved` | HIGH-07 |
| `RemoteBridgeSignerSet` | NEW-01 |

---

## 18. TrustDataBridge — `theloopbreaker-site/contracts-draft/TrustDataBridge.sol`

**Category:** Trust oracle / cross-chain bridge (draft — permissioned relayer model without Teleporter)

### Overview

`TrustDataBridge` is a **draft** contract implementing a simpler cross-chain trust synchronization mechanism without Avalanche Teleporter. It covers ETH Mainnet ↔ Base and ETH Mainnet ↔ Avalanche paths via a permissioned relayer network. Unlike `TrustPortabilityExtensionV3`, it does not implement `ITeleporterReceiver` and uses a separate emit/receive pattern rather than message envelopes.

**Supported data types:**
1. `IdentityAttestation` — from `ERC8004IdentityRegistry`
2. `ReputationScore` — from `VaultfireERC8004Adapter`
3. `VNSRegistration` — VNS name ↔ address
4. `AccountabilityBond` — from `AIAccountabilityBondsV2`
5. `PartnershipBond` — from `AIPartnershipBondsV2`

**Status:** `@custom:status DRAFT — Pending audit before mainnet activation`

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
 34  contract TrustDataBridge {
 41–47  enum DataType { IdentityAttestation, ReputationScore, VNSRegistration,
          AccountabilityBond, PartnershipBond }
 50–58  struct TrustRecord { dataType, subject, dataHash, payload, sourceChainId, timestamp, nonce }
 64  address public owner; bool public paused
 68  uint256 public outboundNonce
 71  mapping(uint256 => uint256) public inboundNonce  // sourceChainId → highest processed nonce
 74  mapping(bytes32 => bool) public processed         // dataHash dedup
 77  mapping(address => bool) public relayers; uint256 public relayerCount
 79  uint256 public requiredRelayers                    // quorum
 82  mapping(uint256 => bool) public supportedChains
 88–104 Storage mappings for 5 data types
107–108 receivedCount[uint8] and emittedCount[uint8]
113–139 Events: TrustDataEmitted (5 fields indexed), TrustDataReceived,
          RelayerAdded/Removed, ChainAdded/Removed, Paused/Unpaused
145–158 Modifiers: onlyOwner, onlyRelayer, whenNotPaused
164–176 constructor: owner=deployer, supports chains 1/8453/43114, owner is first relayer
189–327 emit functions: emitIdentityAttestation, emitReputationScore, emitVNSRegistration,
          emitAccountabilityBond, emitPartnershipBond — all permissionless, all emit TrustDataEmitted
332–384 receiveTrustData: onlyRelayer, validates hash, stores by type
393–444 View functions: has* and get* per data type, getStats
465–508 Admin: addRelayer, removeRelayer, setRequiredRelayers, addChain, removeChain,
          pause, unpause, transferOwnership
514–522 receive() and fallback() — both revert ("no ETH accepted")
```

### Line-by-Line Walkthrough

**Lines 189–327 — Emit functions (permissionless).** Any address (not just registered agents) can emit trust data for relay to another chain. This is intentional: the emit side is just an event log — the actual binding storage happens on `receiveTrustData`. Emit functions compute `dataHash = keccak256(abi.encodePacked(dataType, subject, payload, block.chainid))` and increment `outboundNonce`.

**Lines 332–384 — receiveTrustData (relayer-gated).** Validates:
1. `!processed[dataHash]` — deduplication
2. `subject != address(0)`
3. `supportedChains[sourceChainId]`
4. `nonce >= inboundNonce[sourceChainId]` — rejects stale nonces, allows out-of-order (nonce is a minimum bound, not strict sequence)
5. Hash integrity: recomputes `expectedHash` from the payload and verifies against supplied `dataHash`

Sets `processed[dataHash] = true` and `inboundNonce[sourceChainId] = nonce + 1`. Note: single-relayer quorum — current `requiredRelayers = 1` means one relayer can unilaterally write trust data. The `requiredRelayers` field exists but there is no multi-signature accumulation logic — this is a noted gap for production hardening.

**Lines 354–379 — VNS registration storage.** For `DataType.VNSRegistration`, the storage key is derived as `keccak256(payload)` rather than `subject` — because VNS names have a many-to-one relationship with addresses.

**Lines 465–478 — removeRelayer.** Guards `relayerCount > requiredRelayers` — prevents removing so many relayers that quorum becomes unreachable.

**Lines 514–522 — receive/fallback.** Both explicitly revert, preventing ETH/token accumulation. This is a safety design choice documented in NatSpec.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `owner` | `address` | Admin | `transferOwnership` |
| `paused` | `bool` | Emergency pause | `pause`/`unpause` |
| `outboundNonce` | `uint256` | Monotonic outbound counter | Incremented per emit |
| `inboundNonce` | `mapping(uint256→uint256)` | Highest processed nonce per source chain | Updated per receive |
| `processed` | `mapping(bytes32→bool)` | Processed data hashes | Set on receive |
| `relayers` | `mapping(address→bool)` | Authorized relayers | Admin-controlled |
| `requiredRelayers` | `uint256` | Quorum for receiving data | `setRequiredRelayers` |
| `supportedChains` | `mapping(uint256→bool)` | Allowed source/dest chain IDs | Admin-controlled |
| Storage maps for 5 data types | Various | Received payloads | Written on `receiveTrustData` |

### Events

| Event | Trigger | Key Data |
|---|---|---|
| `TrustDataEmitted` | Each emit function | dataType (indexed), subject (indexed), dataHash, payload, destChainId, nonce, timestamp |
| `TrustDataReceived` | `receiveTrustData` | dataType (indexed), subject (indexed), dataHash, sourceChainId (indexed), nonce, timestamp |
| `RelayerAdded/Removed` | Admin | relayer address |
| `ChainAdded/Removed` | Admin | chainId |
| `Paused/Unpaused` | Admin | by address |

### Security Invariants

1. Hash integrity checked on receive — prevents data tampering
2. Deduplication via `processed[dataHash]`
3. Nonce ordering enforced per source chain
4. Receive is relayer-gated; emit is permissionless
5. **DRAFT GAP:** `requiredRelayers` field exists but no multi-sig accumulation — single relayer can write
6. Contract cannot receive ETH or tokens — both fallback and receive revert
7. Pause mechanism for emergency suspension
8. Chain whitelist prevents cross-chain spam from unsupported chains

### Dependencies + Rebuild Recipe

- **Imports:** None
- **Constructor:** no args
- **Pre-deployment config:** `addRelayer`, `setRequiredRelayers`, `addChain` for non-default chains
- **Status:** DRAFT — not production-ready without multi-relayer accumulation logic

---

## 19. IERC20Minimal — `vf-audit-contracts/IERC20Minimal.sol`

**Category:** Other (minimal ERC-20 interface)

### Full Source

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

/**
 * @title IERC20Minimal
 * @notice Minimal ERC20 interface for VaultFire V3.1 multi-asset staking.
 * @dev Only the functions actually used by BaseYieldPoolBondV3.
 *      Keeps dependency surface small for auditors.
 */
interface IERC20Minimal {
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}
```

### Description

A 4-function minimal ERC-20 interface. Deliberately omits `totalSupply`, `allowance`, and all ERC-20 optional extensions to minimize the auditable surface for `BaseYieldPoolBondV3`'s token interactions. The version in `vf-audit-contracts` adds the NatSpec note "for VaultFire V3.1 multi-asset staking."

All four functions mirror the EIP-20 specification exactly. `transferFrom` and `transfer` return `bool` to allow the caller to check for non-reverting failure modes (required because some tokens like USDT return false instead of reverting).

### Integration

Used by `BaseYieldPoolBondV3`, `AIAccountabilityBondsV3`, `AIPartnershipBondsV3`, and `VaultfireTaskEscrow` (which has its own inline `IERC20` definition with the same three functions).

---

## 20. IERC20Minimal — `v3-pr-ready/contracts/v3/IERC20Minimal.sol`

**Category:** Other (minimal ERC-20 interface — PR-ready variant)

Nearly identical to the audit-contracts variant. The NatSpec describes it as "for multi-asset staking" without the V3.1 qualifier. Identical four-function interface. Both files exist to provide the correct import path for their respective contract directory trees.

---

## 21. VaultfireErrors — `vf-audit-contracts/VaultfireErrors.sol`

**Category:** Other (shared custom error library)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
  4–14  NatSpec: custom errors reduce bytecode; 4-byte selectors vs 32+ bytes/string;
          prefixes: VF_=general/BaseYieldPool, AB_=AIAccountabilityBonds, PB_=AIPartnershipBonds
16–48  BaseYieldPoolBondV3 errors (VF_ prefix):
        VF_NativeOnly, VF_TokenAlreadyAccepted, VF_NameInvalid, VF_TokenNotAccepted,
        VF_TokenTransferFailed, VF_NativeTransferFailed, VF_MustSendValue, VF_AmountZero,
        VF_InsufficientYieldPool, VF_BelowMinimumBalance, VF_TransferFailed,
        VF_MinimumMustBePositive, VF_InsufficientNativeYield, VF_YieldPoolBelowMinimum,
        VF_InsufficientTokenYield, VF_NoAppreciationToDistribute,
        VF_InsufficientYieldPending, VF_InsufficientTokenYieldPend,
        VF_ExceedsProtocolFunds, VF_WithdrawalAlreadyPending, VF_NoWithdrawalPending,
        VF_TimelockNotExpired, VF_InsufficientProtocolFunds, VF_FundWithdrawalFailed,
        VF_TokenTransferApproval, VF_InsufficientBondStake
50–94  AIAccountabilityBondsV3 errors (AB_ prefix): 42 errors covering oracle registration,
        trust scores, metrics verification, distribution, challenge, slashing
96–128 AIPartnershipBondsV3 errors (PB_ prefix): 25 errors covering participants,
        distribution, oracle cross-check, identity, cycle confirmation
```

### Role in the Ecosystem

`VaultfireErrors.sol` is a top-level file (not a contract) that declares free-floating `error` definitions. Any contract in the V3 suite that `import`s this file gains access to all custom errors. This pattern:
1. Eliminates duplicate error definitions across contracts
2. Ensures all reverts use 4-byte selectors (vs. 32+ bytes per string)
3. Makes the error catalog discoverable in one file for auditors

**Gas savings:** Each `require(condition, "string")` costs at minimum 32 bytes of string data plus the require overhead. A custom `error SomeError()` costs 4 bytes (selector only). For a contract with 20 error conditions and 100-byte average strings, switching to custom errors saves ≈ 1,900 bytes of deployed bytecode.

Notable audit-fix additions at the end:
- `AB_OracleAlreadyConfirmed()` — [audit-v3.3] CRIT-08: prevents double-oracle confirmation
- `AB_InsufficientTokenProtocolFunds()` — [audit-v3.3] HIGH-04: ERC-20 challenge penalty gap
- `AB_OracleDeviationTooHigh()` — [audit-v3.3] CRIT-03: oracle cross-check failure
- `AB_ConfirmationStale()` — [audit-v3.3] HIGH-03: cycle-specific confirmation staleness
- `PB_InsufficientPartnershipFund()` — [audit-v3.3] HIGH-05: per-token fund gap
- `PB_NoPendingWithdrawal()` / `PB_WithdrawalTransferFailed()` — [audit-v3.3] HIGH-02: pull pattern
- `PB_AgentNotRegistered()` — [audit-v3.3] HIGH-16: identity check

### Integration

Imported by `BaseYieldPoolBondV3`, `AIAccountabilityBondsV3`, `AIPartnershipBondsV3`. The test-fixes variant (below) is identical in structure but may diverge in specific error additions over time.

---

## 22. VaultfireErrors — `test_fixes/contracts/VaultfireErrors.sol`

**Category:** Other (shared custom error library — test-fixes branch)

Identical structure and prefix conventions to `vf-audit-contracts/VaultfireErrors.sol`. This file exists in the `test_fixes` branch for use by the test-fixed versions of the V3 contracts. The audit-v3.3 additions (CRIT-08, HIGH-04, CRIT-03, HIGH-03, HIGH-05, HIGH-02, HIGH-16) are present in both files, indicating they were back-ported to the test branch after audit findings. No material differences detected.

---

## 23. IVaultfireBridgeNotifier — `vf-audit-contracts/IVaultfireBridgeNotifier.sol`

**Category:** Other (bridge notification interface)

### Full Source

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

/**
 * @title IVaultfireBridgeNotifier
 * @notice Minimal interface for bond contracts to notify the bridge of deactivations.
 * @dev [audit-v3.3] HIGH-18: Bond deactivation must notify the bridge so the remote
 *      chain marks the bond inactive and doesn't serve stale "active" state.
 */
interface IVaultfireBridgeNotifier {
    function notifyPartnershipBondDeactivated(uint256 bondId) external;
    function notifyAccountabilityBondDeactivated(uint256 bondId) external;
}
```

### Description

A 2-function interface that bond contracts call when a bond is deactivated. This addresses **audit finding HIGH-18**: without cross-chain notification, the remote chain's bridge continues serving the bond as "active" after it is deactivated on the source chain, creating a stale-state attack surface.

**Flow:**
1. Bond contract calls `deactivateBond(bondId)` on source chain
2. Within the same transaction, bond contract calls `IVaultfireBridgeNotifier(bridgeAddress).notifyPartnershipBondDeactivated(bondId)` or `notifyAccountabilityBondDeactivated(bondId)`
3. Bridge contract (implementing this interface) emits a deactivation event or sends a cross-chain message
4. Remote chain bridge marks the bond inactive

**Implementing contract:** `VaultfireTeleporterBridgeV3` is the expected implementer.

**Integration:** `AIPartnershipBondsV3` and `AIAccountabilityBondsV3` (in the audit-v3.3 versions) store a `bridgeNotifier: IVaultfireBridgeNotifier` address and call these functions on deactivation.

---

## 24. VaultfireInterface — `theloopbreaker-site/partner_modules/sdk/solidity/VaultfireInterface.sol`

**Category:** Other (partner SDK interface)

### Full Source

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface VaultfireInterface {
    function reward(address user, uint256 amount) external;
}
```

### Description

The minimal Solidity interface exposed to partner integrations via the SDK. A single function: `reward(address user, uint256 amount)`. Partner contracts that want to trigger Vaultfire reward distributions import this interface and call the deployed Vaultfire contract through it.

**Note:** Pragma is `^0.8.0` rather than `^0.8.25` — intended for broad partner compatibility.

**Integration:** This is the partner-facing entry point. The implementing contract (likely `VaultfireRewardStream` or a wrapper) handles the actual reward logic.

---

## 25. Deploy.s.sol — `theloopbreaker-site/risc0/script/Deploy.s.sol`

**Category:** Other (Foundry deployment script)

### Full Source Summary

```
  1  pragma solidity ^0.8.25;
  4  import "forge-std/Script.sol";
  5  import {ProductionBeliefAttestationVerifier} from "../src/ProductionBeliefAttestationVerifier.sol";
  7  /// @title DeployProductionVerifier
  9  /// Usage: forge script script/Deploy.s.sol:DeployProductionVerifier \
 11  ///          --rpc-url $BASE_RPC_URL --broadcast --verify
 14  ///          --etherscan-api-key $BASESCAN_API_KEY
 23  contract DeployProductionVerifier is Script {
 25  address constant BASE_MAINNET_VERIFIER =
 26      0x0b144e07a0826182b6b59788c34b32bfa86fb711;
 28  function run() external {
 30      uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
 31      bytes32 imageId     = vm.envBytes32("IMAGE_ID");
 33      address verifierAddr = vm.envOr("RISC_ZERO_VERIFIER", BASE_MAINNET_VERIFIER);
 39      vm.startBroadcast(deployerKey);
 41      ProductionBeliefAttestationVerifier verifier =
 42          new ProductionBeliefAttestationVerifier(verifierAddr, imageId);
 44      vm.stopBroadcast();
 47      console.log("=== Deployment Complete ===");
 48      console.log("ProductionBeliefAttestationVerifier:", address(verifier));
 49      console.log("RiscZeroVerifier:", verifierAddr);
 51      console.logBytes32(imageId);
 52      console.log("Owner:", verifier.owner());
 53      console.log("Chain ID:", block.chainid);
 55  }
```

### Description

Foundry deployment script for `ProductionBeliefAttestationVerifier`. Not a deployable contract itself — inherits `Script` and is only run via `forge script`. Uses `vm.env*` cheatcodes to read three configuration values:

- `DEPLOYER_PRIVATE_KEY` — the deploying EOA's private key
- `IMAGE_ID` — the RISC Zero guest program image ID (bytes32 hex)
- `RISC_ZERO_VERIFIER` — optional override; defaults to `0x0b14...b711` (Base Mainnet RiscZeroVerifierRouter)

`vm.startBroadcast(deployerKey)` / `vm.stopBroadcast()` marks the section of code that generates actual broadcast transactions. Everything outside that block executes locally only.

**Hard-coded Base Mainnet verifier address:** `0x0b144e07a0826182b6b59788c34b32bfa86fb711`. This should be verified against the official RISC Zero deployment registry before production use.

### Rebuild Recipe

```bash
export DEPLOYER_PRIVATE_KEY=<hex>
export IMAGE_ID=<bytes32_hex>
export BASE_RPC_URL=https://mainnet.base.org
export BASESCAN_API_KEY=<key>

forge script script/Deploy.s.sol:DeployProductionVerifier \
  --rpc-url $BASE_RPC_URL \
  --broadcast \
  --verify \
  --etherscan-api-key $BASESCAN_API_KEY \
  -vvvv
```

---

## 26. SubmitProof.s.sol — `theloopbreaker-site/risc0/script/SubmitProof.s.sol`

**Category:** Other (Foundry proof submission script)

### Full Source Summary

```solidity
contract SubmitProof is Script {
    function run() external {
        uint256 deployerKey  = vm.envUint("DEPLOYER_PRIVATE_KEY");
        address verifierAddr = vm.envAddress("VERIFIER_ADDRESS");
        bytes memory seal    = vm.envBytes("PROOF_SEAL_HEX");
        bytes memory journal = vm.envBytes("PROOF_JOURNAL_HEX");
        
        ProductionBeliefAttestationVerifier verifier =
            ProductionBeliefAttestationVerifier(verifierAddr);
        
        vm.startBroadcast(deployerKey);
        bool success = verifier.verifyAttestation(seal, journal);
        vm.stopBroadcast();
        
        if (success) console.log("=== Attestation Verified Successfully ===");
        else console.log("=== Attestation Verification Failed ===");
    }
}
```

### Description

Foundry script to submit a RISC Zero proof to the deployed `ProductionBeliefAttestationVerifier`. Four environment variables required:

- `DEPLOYER_PRIVATE_KEY` — the submitting account
- `VERIFIER_ADDRESS` — deployed `ProductionBeliefAttestationVerifier` address
- `PROOF_SEAL_HEX` — hex-encoded seal bytes from the RISC Zero prover
- `PROOF_JOURNAL_HEX` — hex-encoded journal bytes from the RISC Zero prover

`verifier.verifyAttestation(seal, journal)` calls the on-chain ZK verifier. On success, it logs the attestation count. Note: the script exits silently on false return rather than reverting — callers should check logs carefully.

### Rebuild Recipe

```bash
export DEPLOYER_PRIVATE_KEY=<hex>
export VERIFIER_ADDRESS=0x...
export PROOF_SEAL_HEX=0x...
export PROOF_JOURNAL_HEX=0x...

forge script script/SubmitProof.s.sol:SubmitProof \
  --rpc-url $BASE_RPC_URL \
  --broadcast \
  -vvvv
```

---

## 27. consent\_contract.sol — `theloopbreaker-site/consent_contract.sol`

**Category:** Privacy guarantees (programmable consent tokens)

### Full Source Summary

```
  1  pragma solidity ^0.8.20;
  6  contract ConsentRegistry {
  7–17  struct ConsentToken { subject, purpose, dataClass, issuedAt, expiresAt,
           usageLimit, usageCount, revoked, ethicsHash }
 19–21  Events: ConsentIssued, ConsentRevoked, ConsentUsed
 23  mapping(uint256 => ConsentToken) private _tokens
 24  uint256 private _nextTokenId = 1
 25  address public immutable ethicsModule
 27–30  modifier onlySubject(uint256 tokenId)
 32–35  constructor(address ethicsModuleAddress)
 37–61  issueConsent(purpose, dataClass, expiresAt, usageLimit, ethicsHash) → tokenId
 63–68  revokeConsent(tokenId): onlySubject, sets revoked=true
 70–75  recordUsage(tokenId, payloadHash): validates + increments usageCount
 77–79  getConsent(tokenId) → ConsentToken
 81–101 isActionAllowed(tokenId, expectedPurpose, expectedDataClass, ethicsHash) → bool
103–117 _validateUsage(token) → bool: internal check for active/not-expired/not-over-limit
```

### Line-by-Line Walkthrough

**Lines 7–17 — ConsentToken struct.** Rich consent token with:
- `subject`: the address that issued consent (the data subject)
- `purpose`: human-readable purpose string (stored on-chain — note this differs from `PrivacyGuarantees.sol` which stores hashes only)
- `dataClass`: the class of data the consent covers
- `issuedAt` / `expiresAt`: uint64 timestamps
- `usageLimit` / `usageCount`: uint32 values enabling a consent token to be "used up" after N operations
- `ethicsHash`: bytes32 hash of the applicable ethics module state at time of issuance

**Line 25 — ethicsModule.** Immutable reference to an ethics compliance contract. Passed in constructor, never changed.

**Lines 37–61 — issueConsent.** Called by `msg.sender` (the data subject). Validates `expiresAt > block.timestamp` and `usageLimit > 0`. Auto-assigns `_nextTokenId++`. Emits `ConsentIssued` with purpose and dataClass strings (on-chain PII exposure — design trade-off vs. `PrivacyGuarantees` which uses hashes).

**Lines 63–68 — revokeConsent.** `onlySubject(tokenId)` — only the original subject can revoke. Sets `revoked = true`.

**Lines 70–75 — recordUsage.** Permissionless — any operator can record a usage event for a valid token. Validates via `_validateUsage`, increments `usageCount`, emits `ConsentUsed` with `payloadHash` (the specific data payload being accessed, hashed for privacy).

**Lines 81–101 — isActionAllowed.** The key gate function for operators. Checks:
1. `_validateUsage` passes
2. Purpose matches exactly (keccak comparison)
3. Data class matches exactly
4. `ethicsHash` matches the stored ethics hash

**Lines 103–117 — _validateUsage.** Internal. Returns false if subject is zero, token is revoked, block.timestamp > expiresAt, or usageCount >= usageLimit.

### State Variables

| Variable | Type | Purpose | Mutability |
|---|---|---|---|
| `_tokens` | `mapping(uint256→ConsentToken)` | Token storage | Written on issue, updated on revoke/usage |
| `_nextTokenId` | `uint256` | Monotonic token ID (starts at 1) | Incremented on issue |
| `ethicsModule` | `address` | Ethics compliance reference | `immutable` |

### Events

| Event | Trigger | Data |
|---|---|---|
| `ConsentIssued` | `issueConsent` | tokenId, subject, purpose, dataClass |
| `ConsentRevoked` | `revokeConsent` | tokenId, subject |
| `ConsentUsed` | `recordUsage` | tokenId, operator, payloadHash |

### Security Invariants

1. Only the subject can revoke their own consent
2. Usage limit is hard-enforced — once exhausted, `_validateUsage` returns false
3. Expiry is timestamp-based
4. `ethicsHash` must match exactly — binding issuance to a specific ethics state
5. **On-chain PII risk:** purpose and dataClass strings stored on-chain (contrast with `PrivacyGuarantees` which stores only hashes)
6. `recordUsage` is permissionless — any address can record usage against a valid token

### Relationship to PrivacyGuarantees

`consent_contract.sol` provides **programmable, usage-limited consent tokens** with rich on-chain metadata. `PrivacyGuarantees.sol` provides a simpler **consent flag registry** using hashed purposes. They are complementary: `ConsentRegistry` is appropriate for permissioned data access scenarios with usage tracking, while `PrivacyGuarantees` is a minimalist floor for all modules.

---

## 28. RewardStream.t.sol — `theloopbreaker-site/test/RewardStream.t.sol`

**Category:** Other (Foundry test contract — reentrancy validation)

### Full Source Summary

```
  1  pragma solidity ^0.8.20;
  4  import "forge-std/Test.sol";
  6  import {RewardStream} from "../contracts/RewardStream.sol";
  8  contract ReentrantReceiver {
  9      RewardStream private immutable _stream;
 10      bool private _blocked;
 12–14  constructor(RewardStream stream)
 16–26  receive() payable: attempts reentrancy on _stream.claimRewards,
          catches revert, sets _blocked=true
 28–30  blocked() external view → bool
 33  contract RewardStreamTest is Test {
 34      RewardStream private stream;
 35–36  constants: ADMIN=0xA11CE, GOVERNOR=0xBEEF
 38–42  setUp(): prank as ADMIN, deploys RewardStream(ADMIN, GOVERNOR), deals 5 ETH to stream
 44–55  testDistributeNoReentry: deploys ReentrantReceiver, queues + distributes 1 ETH,
           asserts receiver got 1 ETH, asserts blocked=true (attempted reentry)
```

### Description

A Foundry test that specifically validates reentrancy protection in `RewardStream.claimRewards`. The `ReentrantReceiver` contract attempts reentrancy by calling `claimRewards` in its `receive()` fallback. The test asserts:
1. The receiver ultimately received 1 ETH (the distribution succeeds)
2. The receiver's `_blocked` flag is true (reentrancy was attempted once — guarded by `_blocked` to avoid infinite loops)
3. The reentrancy `try` block caught a revert (either from reentrancy guard or "nothing to claim")

**Test pattern:** Foundry `vm.prank(ADMIN)` sets `msg.sender` for the next call. `vm.deal(address(stream), 5 ether)` directly loads the contract balance without a transfer call. `assertEq` and `assertTrue` are Foundry assertion functions.

**What it tests:** That `RewardStream.claimRewards` cannot be re-entered to double-claim rewards, and that the primary distribution path still succeeds even when the recipient tries reentrancy.

### Dependencies

- `forge-std/Test.sol` — Foundry test framework
- `RewardStream` — the contract under test (see `theloopbreaker-site/contracts/RewardStream.sol`)

---

## Cross-Contract Integration Map

```
MultisigGovernance / MultisigGovernanceV3
    ← is referenced by → VaultfireTaskEscrow.governance (dispute resolver)
    ← should own → MissionEnforcement.owner
    ← should own → AntiSurveillance.owner
    ← should own → VaultfireDisputeResolution.owner
    ← should own → VaultfireReputationStaking.owner (slash authority)
    ← should own → VaultfireBondInsurancePool.owner (claim resolver)
    ← should own → TrustPortabilityExtensionV3.owner

ERC8004IdentityRegistry (not in this batch)
    → isAgentActive() called by:
        VaultfireTaskEscrow (onlyRegisteredAgent)
        VaultfireDisputeResolution (onlyRegisteredAgent)
        VaultfireCapabilityCredentials (issueCredential guard)
        VaultfireReputationStaking (vouch, slashAgent)
        VaultfireBondInsurancePool (contribute, submitClaim)
        VaultfireTrustOracle (submitAttestation)

VaultfireTaskEscrow
    → issues task completions that feed into →
        VaultfireCapabilityCredentials.addTaskProof (via issuer)
        VaultfireTrustOracle.submitAttestation (via task poster)

VaultfireReputationStaking
    → profile data (totalStakeReceived, slashCount) feeds into →
        TrustPortabilityExtensionV3 (trust tier computation)

VaultfireTrustOracle
    → averageQuality, verifiedAttestations feed into →
        TrustPortabilityExtensionV3.sendTrustTier (averageRating, verifiedFeedbacks)

TrustPortabilityExtensionV3
    ← wraps → Avalanche ITeleporterMessenger for cross-chain sends
    → syncs to/from → remote chain TrustPortabilityExtensionV3 instance

IVaultfireBridgeNotifier
    → implemented by → VaultfireTeleporterBridgeV3 (Batch 7)
    → called by → AIPartnershipBondsV3 / AIAccountabilityBondsV3 on deactivation

MissionEnforcement
    ← verifyAIProfitCap() called by → AIAccountabilityBondsV2 / AIPartnershipBondsV2
    ← enforceMission modifier used by → bond contracts for principle gating

AntiSurveillance
    → surveillanceFreeOnly modifier available to → composing modules

PrivacyGuarantees
    → requiresConsent modifier available to → composing modules
    → checkDeletionRequest modifier available to → ERC8004 registries

VaultfireForumRegistry
    ← read by → Vaultfire Discussion Board frontend
    → no on-chain callers — view-only integration

VaultfireBondInsurancePool
    → isInsured(agent) readable by → task routing layer
    → funded by contributions + slashed ETH from VaultfireReputationStaking (indirectly)

IERC20Minimal / VaultfireErrors / IVaultfireBridgeNotifier
    → utility types imported by V3 bond contracts

VaultfireInterface (SDK)
    ← implemented by → VaultfireRewardStream
    → used by → partner module smart contracts
```

---

## Appendix — Rebuild Recipes

### MultisigGovernance (V1)
```solidity
constructor(address[] memory _signers, uint256 _threshold)
// No imports needed
```

### MultisigGovernanceV3 (V3, all timelock variants)
```solidity
// Import: @openzeppelin/contracts/utils/ReentrancyGuard.sol
constructor(address[] memory _signers, uint256 _threshold, address[3] memory _guardians)
// threshold must satisfy: threshold * 10000 / signers.length >= 5000
```

### MultisigGovernanceV3 (v3-pr-ready — Gnosis-style)
```solidity
// No imports
constructor(address[] memory _signers, uint256 _threshold)
// threshold must be >= 2, signers <= 20
```

### TrustPortabilityExtensionV3
```solidity
// Import: teleporter/ITeleporterMessenger.sol, teleporter/ITeleporterReceiver.sol
constructor(address _teleporterMessenger, uint256 _requiredGasLimit)
// Post-deploy: setRemoteBridge(), addRelayer() as needed
// V3.3 only: setRemoteBridgeSigner(), addTrustedVerifierImageId()
```

### VaultfireTaskEscrow
```solidity
constructor(
    address identityRegistry_,  // ERC8004IdentityRegistry
    address governance_,         // MultisigGovernance
    address usdc_,               // USDC token contract
    address feeRecipient_        // Protocol fee address
)
```

### VaultfireDisputeResolution
```solidity
constructor(address identityRegistry_)
// Owner is deployer; should transfer to MultisigGovernance
```

### VaultfireCapabilityCredentials
```solidity
constructor(address identityRegistry_)
// Owner is deployer; can approve additional issuers via approveIssuer
```

### VaultfireReputationStaking
```solidity
constructor(address identityRegistry_)
// Owner is deployer; owner = slash authority
```

### VaultfireTrustOracle
```solidity
constructor(address identityRegistry_)
// No owner; fully permissionless
```

### VaultfireForumRegistry
```solidity
// No constructor args; no owner; permissionless
```

### VaultfireBondInsurancePool
```solidity
constructor(address identityRegistry_)
// Owner is deployer; owner = claim resolver
```

### TrustDataBridge
```solidity
// No constructor args; deployer becomes owner + first relayer
// Post-deploy: addRelayer(), setRequiredRelayers(), configure chains
// DRAFT — not production-ready
```

### ConsentRegistry
```solidity
constructor(address ethicsModuleAddress)
// ethicsModuleAddress must be non-zero
```

---

*End of Vaultfire Codex Batch 6 — Governance, Privacy, and Miscellaneous Contracts*


<div style="page-break-before:always"></div>

# 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.


<div style="page-break-before:always"></div>

# Vaultfire Codex — Batch 8: Deprecated Contract Archive

> **⚠ DEPRECATED WARNING**
>
> These contracts are archived and not used in the current Vaultfire system. They are documented here for historical completeness. None of the contracts in this file should be deployed, referenced in new code, or treated as canonical implementations. Where a V2 successor exists it supersedes the corresponding entry below in every respect.

---

## Table of Contents

1. [PurchasingPowerBondsV2.sol](#1-purchasingpowerbondsv2sol)
2. [FanBeliefBond.sol](#2-fanbeliefbondsol)
3. [VaultfireDAO.sol](#3-vaultfiredaosol)
4. [VerdantAnchorBondsV2.sol](#4-verdantanchorbondsv2sol)
5. [CommonGroundBonds.sol](#5-commongroundbondssol)
6. [SwapGate.sol](#6-swapgatesol)
7. [BuilderBeliefBondsV2.sol](#7-builderbeliefbondsv2sol)
8. [VaultfireRewardStream.sol](#8-vaultfirerewardstreamsol)
9. [VerdantAnchorBonds.sol](#9-verdantanchorbondssol)
10. [FreedomVow.sol](#10-freedomvowsol)
11. [HealthCommonsBondsV2.sol](#11-healthcommonsbondsv2sol)
12. [CompetitiveIntegrityBond.sol](#12-competitiveintegritybondsol)
13. [EscapeVelocityBonds.sol](#13-escapevelocitybondssol)
14. [GhostkeyAttribution.sol](#14-ghostkeyattributionsol)
15. [RewardMultiplier.sol](#15-rewardmultipliersol)
16. [TeamworkIntegrityBond.sol](#16-teamworkintegritybondsol)
17. [RewardStream.sol](#17-rewardstreamsol)
18. [ContributorUnlockKey.sol](#18-contributorunlockkeysol)
19. [GhostkeyLoyaltyLock.sol](#19-ghostkeyloyaltylocksol)
20. [LaborDignityBonds.sol](#20-labordignityaondssol)
21. [EscapeVelocityBondsV2.sol](#21-escapevelocitybondsv2sol)
22. [YieldPoolHealthMonitor.sol](#22-yieldpoolhealthmonitorsol)
23. [LaborDignityBondsV2.sol](#23-labordignityabondsv2sol)
24. [CommonGroundBondsV2.sol](#24-commongroundbondsv2sol)
25. [CovenantFlame.sol](#25-covenantflamesol)
26. [PurchasingPowerBonds.sol](#26-purchasingpowerbondssol)
27. [AIAccountabilityBonds.sol](#27-aiaccountabilitybondssol)
28. [BuilderBeliefBonds.sol](#28-builderbeliefbondssol)
29. [AIPartnershipBonds.sol](#29-aipartnershipbondssol)
30. [FanVerificationSybilResistance.sol](#30-fanverificationsybilresistancesol)
31. [VaultfireBugBounty.sol](#31-vaultfirebugbountysol)
32. [VaultfireLoyalty.sol](#32-vaultfireloyaltysol)
33. [HealthCommonsBonds.sol](#33-healthcommonsbondssol)

---

## Shared Terminology

Throughout this document the following terms appear repeatedly:

- **BaseYieldPoolBond** — an inherited base contract (not deprecated; still active) that provides `DISTRIBUTION_TIMELOCK`, `_useYieldPool()`, `_replenishYieldPool()`, `_validateNonZero()`, `_validateScore()`, `_validateAddress()`, the `whenNotPaused` modifier, `nonReentrant`, and `onlyOwner`.
- **MissionEnforcement** — an optional gating contract that checks whether a calling contract is compliant with named `CorePrinciple` values (e.g., `OPEN_SOURCE_VERIFIABLE`, `PRIVACY_DEFAULT`, `AI_PROFIT_CAPS`).
- **`.transfer()` pattern** — the Solidity 2300-gas-stipend ETH transfer method. Deprecated in favour of `.call{value: …}("")` because `transfer` can silently fail when the recipient is a smart contract with a non-trivial receive function, or when future EVM repricing changes gas costs.
- **Basis points (bps)** — 1/100th of a percent. 10 000 bps = 100 %. Used throughout for fractional multipliers and percentage calculations.
- **Score range 0–10 000** — the canonical representation of a percentage where 10 000 = 100 %. All raw metric values in these contracts use this scale unless otherwise stated.
- **Math-fix 2026-01-07** — a divisor correction applied across several V2 bond contracts on that date to recalibrate bond-value outputs to realistic ETH magnitudes.

---

---

# 1. PurchasingPowerBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/PurchasingPowerBondsV2.sol`
**Lines:** ~751
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

Companies stake ETH that grows in value when workers can afford a 1990s-equivalent standard of living — housing, food, healthcare, and discretionary spending. The contract operationalises the idea that employer profit-sharing should be pegged to workers' real purchasing power, not nominal wage levels. If workers cannot afford basic necessities at 1990-price parity the company loses its appreciation share; workers receive 100 % by penalty.

## Why It Was Likely Deprecated

This is the **V2 successor** to `PurchasingPowerBonds.sol` (V1). It is itself deprecated because the affordability-scoring methodology was seen as insufficiently decentralised — the AI company still self-reports metrics, and the attestation quorum was not enforced on-chain with cryptographic weight. The contract was absorbed into a redesigned scoring oracle system.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
```
Standard SPDX identifier; MIT licence.

```
L2    pragma solidity ^0.8.25;
```
Requires the compiler to be ≥ 0.8.25, capturing all built-in overflow protection and the `unchecked` keyword improvements of that release series.

```
L4    import "../BaseYieldPoolBond.sol";
```
Pulls in the shared base class that supplies `DISTRIBUTION_TIMELOCK`, `_useYieldPool()`, `whenNotPaused`, `nonReentrant`, `onlyOwner`, and helper validators.

```
L5    import "../MissionEnforcement.sol";
```
Optional mission-gate interface. If `missionEnforcementEnabled` is true, `distributeBond` will revert unless the contract is compliant with configured principles.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `nextBondId` | `uint256` | Auto-incrementing ID counter; starts at 1. |
| `bonds` | `mapping(uint256 => Bond)` | Primary storage for each bond keyed by ID. |
| `affordabilityMetrics` | `mapping(uint256 => AffordabilityMetrics[])` | Timestamped affordability readings per bond. |
| `workerAttestations` | `mapping(uint256 => WorkerAttestation[])` | Worker attestations confirming or challenging metrics. |
| `bondDistributions` | `mapping(uint256 => Distribution[])` | Historical distributions per bond. |
| `PENALTY_PURCHASING_POWER` | `uint256` constant = `80` | Minimum purchasing power score below which penalty fires. |
| `PENALTY_WORKER_VERIFICATION` | `uint256` constant = `70` | Minimum worker verification score below which penalty fires. |

---

### Structs

**`Bond`**
```
bondId            uint256  — unique identifier
company           address  — company that staked ETH
companyName       string   — human-readable name
stakeAmount       uint256  — ETH locked at creation
createdAt         uint256  — block.timestamp at creation
active            bool     — false after settlement
```

**`AffordabilityMetrics`**
```
timestamp               uint256  — when reading was recorded
housingAffordability    uint256  — 0-10000 (can workers afford 1990-equivalent housing?)
foodAffordability       uint256  — 0-10000
healthcareAffordability uint256  — 0-10000
discretionaryAffordability uint256 — 0-10000
submitter               address  — who submitted this reading
```

**`WorkerAttestation`**
```
timestamp       uint256  — attestation time
worker          address  — attesting worker
confirmsScore   bool     — does worker confirm company's metric?
workerScore     uint256  — worker's own affordability estimate (0-10000)
notes           string   — free-text attestation notes
```

**`Distribution`**
```
timestamp       uint256  — settlement time
totalAmount     int256   — appreciation amount (may be negative)
workerShare     uint256  — ETH sent to workers/yield pool
companyShare    uint256  — ETH returned to company
reason          string   — human-readable reason string
```

---

### Functions

#### `createBond(companyName, workerCount)`
- `payable`, open to anyone
- Requires `msg.value > 0`
- Stores new `Bond` with `stakeAmount = msg.value`
- Emits `BondCreated(bondId, company, stakeAmount)`
- Returns the new `bondId`

**Design intent:** Companies call this to put skin in the game — ETH they can only retrieve if workers' purchasing power is high enough.

#### `submitAffordabilityMetrics(bondId, housing, food, healthcare, discretionary)`
- Only callable by `bonds[bondId].company`
- Appends a new `AffordabilityMetrics` entry
- All scores clamped/validated to be ≤ 10 000
- Emits `MetricsSubmitted(bondId, timestamp)`

#### `addWorkerAttestation(bondId, confirmsScore, workerScore, notes)`
- Open to any address (intended to be called by actual workers)
- Appends a `WorkerAttestation` entry
- Emits `WorkerAttested(bondId, worker)`

**No Sybil resistance exists for attestations.** Any address may attest, which was a primary design flaw.

#### `purchasingPowerScore(bondId) → uint256`
Returns the average of the four affordability dimensions from the **latest** `AffordabilityMetrics` entry:
```
score = (housing + food + healthcare + discretionary) / 4
```
Returns 5 000 (neutral) if no metrics have been submitted.

#### `workerVerificationScore(bondId) → uint256`
Iterates recent `WorkerAttestation` entries (within the last 180 days). For each:
- If `confirmsScore = true`, uses the company's latest affordability metrics as the worker's score
- Otherwise uses `workerScore` from the attestation itself

Averages all collected scores. Returns 50 (minimum) if fewer than one attestation exists. Maps the average to the range 50–150 via:
```
if avgScore >= 7000  → return 150
if avgScore >= 4000  → return 100 + (avgScore - 4000) / 60
else                 → return 50 + avgScore / 80
```

#### `timeMultiplier(bondId) → uint256`
```
age = block.timestamp − bond.createdAt
years = age / 31 536 000

years < 1   → 100
years 1–3   → 100 + years × 50
years > 3   → 200 + (years − 3) × 50
```
Rewards long-duration bonds; uncapped above year 3 (a potential overflow risk for very long-lived bonds, though practically negligible).

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stakeAmount × purchasingPower × workerVerification × time) / 1_000_000
```
- `purchasingPower` is 0–10 000
- `workerVerification` is 50–150
- `time` is 100–200+
- Divisor 1 000 000 normalises the product back to ETH-magnitude

**Example:** 1 ETH stake, score 7 000, verification 120, time 150 → `(1e18 × 7000 × 120 × 150) / 1_000_000 = 126e18 ETH` — clearly a calibration error demonstrating why the divisor comment "math-fix kept original" is important context.

#### `calculateAppreciation(bondId) → int256`
```
appreciation = int256(calculateBondValue) − int256(stakeAmount)
```
May be negative if scores have declined.

#### `shouldApplyPenalty(bondId) → (bool, string)`
```
purchasingPower < 80  → (true, "Workers cannot afford basics")
workerVerification < 70 → (true, "Workers not verifying claims")
```

#### `distributeBond(bondId)`
- `nonReentrant`, `whenNotPaused`, `onlyAICompany(bondId)` (actually `onlyCompany`)
- Calls `_requireMissionCompliance()` if mission enforcement is enabled
- Checks `DISTRIBUTION_TIMELOCK` (from `BaseYieldPoolBond`)
- Computes appreciation; requires `appreciation != 0`
- Evaluates penalty
- On **normal appreciation (no penalty):** 70 % to workers (via `_useYieldPool`), 30 % to company
- On **penalty:** 100 % to workers
- On **depreciation (negative appreciation):** 100 % workers as compensation, 0 % company
- Calls `_useYieldPool(workerShare)` to route worker ETH through the yield pool
- Emits `BondDistributed(bondId, workerShare, companyShare)`

**Note:** The actual ETH transfer to individual workers is not performed on-chain; it routes to the yield pool and relies on an off-chain worker-registry to distribute further. This is explicitly acknowledged in the V1 source and carried forward as a known limitation.

---

### Math Summary

| Computation | Formula |
|---|---|
| Purchasing power score | `(housing + food + healthcare + discretionary) / 4` |
| Worker verification multiplier | Segmented: 50–150 based on avg worker score |
| Time multiplier | 100 + 50/year up to year 3, then 200 + 50/year beyond |
| Bond value | `(stake × power × verification × time) / 1_000_000` |
| Appreciation | `bondValue − stakeAmount` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, company, stakeAmount)` | `createBond` |
| `MetricsSubmitted(bondId, timestamp)` | `submitAffordabilityMetrics` |
| `WorkerAttested(bondId, worker)` | `addWorkerAttestation` |
| `BondDistributed(bondId, workerShare, companyShare)` | `distributeBond` |
| `PenaltyApplied(bondId, reason)` | `distributeBond` when penalty triggers |
| `MissionEnforcementUpdated(previous, current)` | `setMissionEnforcement` |
| `MissionEnforcementEnabled(bool)` | `setMissionEnforcementEnabled` |

---

### Design Intent

To create a financial instrument where employer capital appreciates only if worker purchasing power is objectively maintained. The 1990-parity benchmark was chosen because that era is broadly considered the last period of broadly distributed wage-cost parity in the US. The goal was to make employers directly financially motivated to keep wages real.

### Why It Didn't Make Production

1. **No Sybil resistance on worker attestations** — any address can attest as a worker. The attestation pool is trivially gameable.
2. **Worker distribution is off-chain** — ETH goes to a yield pool with no on-chain mechanism to route it to individual workers; requires a centralised worker registry.
3. **Superseded by a redesigned oracle** — the scoring methodology was moved to a purpose-built oracle contract with stronger verification.
4. **Divisor calibration** — the 1 000 000 divisor produces unrealistic bond values at mid-range scores (see math section).

---

---

# 2. FanBeliefBond.sol

**Path:** `theloopbreaker-site/contracts/deprecated/FanBeliefBond.sol`
**Lines:** ~663
**Inherits:** `ReentrancyGuard` only (standalone)

## What It Was Supposed to Do

Allow sports fans to stake ETH on athletes or teams maintaining competitive integrity over 1–10 year bond periods. The contract tracks five integrity statuses and five violation types. Proven violations slash fan stakes and reward whistleblowers 10 %. High-integrity athletes over time yield multiplied returns.

## Why It Was Likely Deprecated

Sports integrity data cannot be reliably sourced on-chain without a trusted oracle, and none was wired in. The `IntegrityStatus` is set by an `investigator` address — a single-point centralisation risk. The bond also uses the deprecated `.transfer()` pattern.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
```
Standalone bond — no base class, only reentrancy protection.

---

### Enums

**`IntegrityStatus`**
```
Corrupted   = 0  — athlete has confirmed violations
Declining   = 1  — integrity score falling
Stable      = 2  — neutral
Rising      = 3  — improving
Elite       = 4  — sustained excellence
```

**`ViolationType`**
```
MatchFixing        = 0  — 100 % stake forfeit
PEDUsage           = 1  — 80 % forfeit
GamblingViolation  = 2  — 70 % forfeit
LoadManagementAbuse = 3 — penalty (amount not specified in enum; moderate)
TankingParticipation = 4 — penalty
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `nextBondId` | `uint256` | Bond ID counter. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `integrityRecords` | `mapping(uint256 => IntegrityRecord[])` | Integrity score history per bond. |
| `violationReports` | `mapping(uint256 => ViolationReport[])` | Filed violation reports. |
| `yieldPool` | `uint256` | Separate pool funding appreciation payouts. |
| `owner` | `address` | Contract owner. |
| `pendingOwner` | `address` | Two-step ownership transfer target. |
| `WHISTLEBLOWER_REWARD_BPS` | `uint256` constant = `1000` | 10 % of slashed amount to whistleblower. |
| `MIN_MATURITY` | `uint256` constant | 1 year in seconds. |
| `MAX_MATURITY` | `uint256` constant | 10 years in seconds. |

---

### Structs

**`Bond`**
```
bondId          uint256  — unique ID
fan             address  — staker
athleteOrTeam   string   — subject of the bond
stakeAmount     uint256  — ETH staked
createdAt       uint256  — creation timestamp
maturityDate    uint256  — earliest settlement date
active          bool
status          IntegrityStatus  — current status
investigator    address  — address allowed to set status
```

**`IntegrityRecord`**
```
timestamp   uint256
score       uint256  — 0-10000
status      IntegrityStatus
verifier    address
notes       string
```

**`ViolationReport`**
```
timestamp       uint256
reporter        address
violationType   ViolationType
evidence        string  — off-chain evidence hash or description
confirmed       bool    — set true by investigator
```

---

### Functions

#### `createBond(athleteOrTeam, maturitySeconds, investigator)`
- `payable`; requires `msg.value > 0`
- `maturitySeconds` must be within `[MIN_MATURITY, MAX_MATURITY]`
- Sets initial `IntegrityStatus` to `Stable`
- Emits `BondCreated`

#### `addIntegrityRecord(bondId, score, status, notes)`
- Only callable by `bonds[bondId].investigator`
- Appends a new `IntegrityRecord`
- Updates `bonds[bondId].status`

#### `reportViolation(bondId, violationType, evidence)`
- Open to any address (intended for fans/whistleblowers)
- Appends a pending `ViolationReport`

#### `confirmViolation(bondId, reportIndex)`
- Only investigator
- Sets `violationReports[bondId][reportIndex].confirmed = true`
- Triggers stake forfeit based on `violationType`:
  - `MatchFixing` → 100 % forfeited; 10 % of that to reporter as whistleblower reward
  - `PEDUsage` → 80 % forfeited; 10 % of that to reporter
  - `GamblingViolation` → 70 % forfeited; 10 % to reporter
  - Others → smaller configurable penalties

**Forfeit mechanic:** Slashed amounts are transferred from `bond.stakeAmount` to a `penaltyPool` stored in the contract, minus the whistleblower reward. The whistleblower reward is sent immediately via `.transfer()`.

#### `averageIntegrityScore(bondId) → uint256`
Iterates all `integrityRecords` for the bond; returns the arithmetic mean score. Returns 5 000 if no records.

#### `consistencyScore(bondId) → uint256`
Measures inverse variance. Computes the mean, then sums squared deviations, then returns:
```
consistencyScore = 10000 − min(10000, variance / 10)
```
Higher score = more consistent = more predictable athlete. Returns 10 000 if fewer than two records.

#### `timeMultiplier(bondId) → uint256`
```
elapsed = maturityDate − createdAt
yearsHeld = elapsed / 31_536_000
multiplier = 100 + (yearsHeld × 80)   (range 100–500 capped at 5× for 5-year max)
```

#### `integrityStatusMultiplier(status) → uint256`
```
Elite      → 200
Rising     → 150
Stable     → 100
Declining  → 75
Corrupted  → 0
```

#### `calculateSettlement(bondId) → uint256`
```
fanShare = bond.stakeAmount
         × statusMultiplier(bond.status) / 100
         × (1 + consistencyBonus / 100)
         × timeMultiplier / 100
```
Plus, if `averageIntegrityScore ≥ 8 000` and bond is ≥ 5 years old, adds a **5-year clean record bonus** of `stakeAmount × 20 / 100`.

Appreciation above `stakeAmount` is drawn from `yieldPool`.

#### `settleBond(bondId)`
- `nonReentrant`
- Requires `block.timestamp >= bond.maturityDate`
- Computes settlement; sends fan their share via `.transfer()`
- Emits `BondSettled`

#### `donateToYieldPool()`
- `payable`, open to anyone
- Increases `yieldPool`

#### `transferOwnership(newOwner)` / `acceptOwnership()`
Two-step ownership transfer pattern.

---

### Math Summary

| Computation | Formula |
|---|---|
| Average integrity score | `sum(records) / count` |
| Consistency score | `10000 − min(10000, variance / 10)` |
| Time multiplier | `100 + years × 80`, range 100–500 |
| Status multiplier | Lookup: 0 / 75 / 100 / 150 / 200 |
| Settlement | `stake × status/100 × consistency × time / 100` |
| Whistleblower reward | `forfeitAmount × 10 / 100` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, fan, athleteOrTeam, stakeAmount, maturityDate)` | `createBond` |
| `IntegrityRecordAdded(bondId, score, status)` | `addIntegrityRecord` |
| `ViolationReported(bondId, reporter, type)` | `reportViolation` |
| `ViolationConfirmed(bondId, reportIndex, penaltyAmount)` | `confirmViolation` |
| `WhistleblowerRewarded(reporter, amount)` | `confirmViolation` |
| `BondSettled(bondId, fan, amount)` | `settleBond` |

---

### Design Intent

Create a financial incentive for fans to hold athletes accountable over multi-year windows. Whistleblower rewards create a self-policing ecosystem where bad actors are economically exposed. Elite athletes who maintain clean records over five years reward patient believers with up to 2× appreciation.

### Why It Didn't Make Production

1. **Centralised investigator** — a single `investigator` address has unilateral power to confirm violations and change integrity status. No multi-sig or oracle quorum.
2. **Unimplemented fanConsensus** — the `_calculateFanConsensusScore` function referenced in settlement always returns 5 000 (hardcoded stub). The full fan-consensus scoring layer was never built.
3. **Deprecated `.transfer()` pattern** — throughout the settlement and whistleblower payout paths.
4. **No oracle for sports data** — integrity data is entirely manual and off-chain.
5. **Yield pool funding** — appreciation beyond initial stake must come from a separately donated `yieldPool` with no automated replenishment mechanism.

---

---

# 3. VaultfireDAO.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VaultfireDAO.sol`
**Lines:** 219
**Contains:** Two contracts — `VaultfireGuardianToken` and `VaultfireDAO`

## What It Was Supposed to Do

Provides the on-chain governance layer for the Vaultfire system. `VaultfireGuardianToken` is an ERC-20 voting token minted by attested guardians. `VaultfireDAO` is a full OpenZeppelin Governor that coordinates guardian votes and, when a proposal passes, can call `publishMissionEvo()` on the base oracle to record new system virtue/weight configurations.

## Why It Was Likely Deprecated

The DAO was archived because the guardian attestation model (off-chain attestation → on-chain mint) was redesigned to use Dilithium-based zero-knowledge attestations rather than a simple `ATTESTOR_ROLE` mint. The two-contract file structure was also refactored into separate files.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3–L16 imports
```
Pulls in the full OpenZeppelin Governor stack plus ERC20Votes extensions. Notably imports `Nonces` because both `ERC20Permit` and `ERC20Votes` define a `nonces()` function — the explicit import resolves the ambiguity.

---

### Contract 1: `VaultfireGuardianToken`

**Inheritance chain:** `AccessControl`, `ERC20Burnable`, `ERC20Permit`, `ERC20Votes`

```
L20    bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE");
```
Role that authorises an address to mint guardian voting weight. Intended for a trusted attestation service or multisig.

```
L22    event GuardianAttested(address guardian, uint256 weight, string memo, uint256 newBalance);
```
Emitted whenever voting weight is minted; `memo` carries the off-chain attestation reference.

```
L24–L30 constructor(address admin)
```
Initialises ERC20 as "Vaultfire Guardian" (VFG) and ERC20Permit with domain "Vaultfire Guardian". Grants `DEFAULT_ADMIN_ROLE` to `admin`.

```
L33–L43 function attest(address guardian, uint256 weight, string memo)
```
- Requires `ATTESTOR_ROLE`
- Calls `_mint(guardian, weight)` — increases the guardian's voting power
- `weight` is denominated in raw token units; the calling attestor decides the magnitude
- Returns `newBalance` for convenience
- Emits `GuardianAttested`

```
L45–L50 function _update(address from, address to, uint256 amount)
```
Required override to reconcile `ERC20` and `ERC20Votes`. Delegates to `super._update` which triggers the checkpoint logic in `ERC20Votes`.

```
L52–L58 function nonces(address owner)
```
Overrides both `ERC20Permit` and `Nonces` to resolve the diamond-inheritance conflict. Delegates to `super.nonces`.

---

### Contract 2: `VaultfireDAO`

**Inheritance chain:** `Governor`, `GovernorSettings`, `GovernorCountingSimple`, `GovernorVotes`, `GovernorVotesQuorumFraction`, `GovernorTimelockControl`

```
L73    uint256 public constant RESONANCE_APPROVAL_BPS = 5_100;
```
Proposals must receive at least 51 % of total participating votes (for + against + abstain) to succeed — a stricter threshold than simple majority of for-vs-against.

```
L74    VaultfireGuardianToken public immutable guardianToken;
L75    address public immutable baseOracle;
```
Stored at construction time; immutable so neither can be changed post-deployment (intentional governance rigidity).

```
L92    GovernorSettings(14_400, 50_400, 0)
```
- Voting delay: 14 400 blocks (~2 days at 12 s/block)
- Voting period: 50 400 blocks (~7 days)
- Proposal threshold: 0 tokens (any guardian can propose)

```
L94    GovernorVotesQuorumFraction(4)
```
4 % quorum — at least 4 % of total supply must vote for a proposal to be valid.

---

#### `proposeMissionEvo(virtues, weights, description) → proposalId`

```
L105   require(virtues.length == weights.length, "VaultfireDAO:length");
L106   require(virtues.length != 0, "VaultfireDAO:empty");
```
Validates that virtue names and weights arrays are parallel and non-empty.

```
L113–L120
```
Constructs a standard Governor proposal with a single action:
- Target: `baseOracle`
- Value: 0 ETH
- Calldata: `abi.encodeWithSignature("publishMissionEvo(string[],uint256[])", virtues, weights)`

This pattern encodes the function call at proposal time rather than execution time, which means the exact virtue configuration is visible and auditable in the proposal before any vote is cast.

```
L122   proposalId = propose(targets, values, calldatas, description);
```
Delegates to the standard Governor `propose`, which checks quorum, records the proposal, and starts the voting delay period.

---

#### `_voteSucceeded(proposalId) → bool`

```
L202   (againstVotes, forVotes, abstainVotes) = proposalVotes(proposalId);
L203   total = againstVotes + forVotes + abstainVotes;
L204   if (total == 0) return false;
L207   return forVotes * 10_000 >= total * RESONANCE_APPROVAL_BPS;
```
This is the critical deviation from the OpenZeppelin default. The default `GovernorCountingSimple._voteSucceeded` checks only `forVotes > againstVotes`. This override checks `forVotes / total ≥ 51 %`, meaning a strong abstain-vote bloc can defeat a proposal even if for > against. This was intentional — Vaultfire governance requires affirmative supermajority participation, not just relative support.

**Math:** `forVotes × 10 000 ≥ total × 5 100` resolves to `forVotes / total ≥ 0.51`.

---

### Override Boilerplate (L136–L217)

The remaining functions (`proposalNeedsQueuing`, `_queueOperations`, `_executeOperations`, `_cancel`, `_executor`, `proposalThreshold`, `state`, `supportsInterface`) are required inheritance disambiguation overrides. Each calls `super.*` to delegate to the appropriate parent implementation per OpenZeppelin's linearisation order. No custom logic exists in any of these.

---

### Events

| Event | Trigger |
|---|---|
| `GuardianAttested(guardian, weight, memo, newBalance)` | `VaultfireGuardianToken.attest` |
| `ProposalCreated(id, proposer)` | `VaultfireDAO.propose` |
| `MissionEvoProposal(id, proposer, virtues, weights, description)` | `proposeMissionEvo` |

Plus all standard Governor events (`ProposalQueued`, `ProposalExecuted`, `VoteCast`, etc.) inherited from OpenZeppelin.

---

### Design Intent

On-chain governance where the guardian council — humans who have earned voting weight through real-world attestations — can collectively update the Vaultfire system's virtue weighting. The `RESONANCE_APPROVAL_BPS` threshold ensures changes require genuine broad consent, not just a narrow plurality.

### Why It Didn't Make Production

1. **ATTESTOR_ROLE is a centralisation risk** — one compromised attestor address can mint unlimited voting weight.
2. **Redesign to ZK attestation** — the attestation model was replaced with Dilithium + Groth16 zero-knowledge proofs. A new DAO version was written to consume those proofs directly.
3. **Two-contract file structure** — during refactoring the guardian token and DAO were separated into individual files for better auditability and deployment management.
4. **Proposal threshold = 0** — any guardian token holder can propose, which in combination with a permissioned `ATTESTOR_ROLE` concentrates power in whoever controls attestation.

---

---

# 4. VerdantAnchorBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VerdantAnchorBondsV2.sol`
**Lines:** ~427
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

V2 of the earth-regeneration bond. A landowner and regenerator partner to restore ecological health across five metrics. The contract pays the regenerator for real, verified physical work; the landowner receives 40 % of appreciation. A greenwashing penalty (less than 50 % of verifications confirming physical work) zeroes the regenerator's share.

## Why It Was Likely Deprecated

The `OPEN_SOURCE` and `PRIVACY_DEFAULT` mission checks in V2 are unrelated to land stewardship and their inclusion was a copy-paste artifact from a builder-focused template. The architectural decision to route everything through `BaseYieldPoolBond` also meant the yield pool needed funding from a source that was never defined. Superseded by a revised version integrating a dedicated ecological oracle.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L3   SPDX, pragma, imports (BaseYieldPoolBond)
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `ecologicalMetrics` | `mapping(uint256 => EcologicalMetrics[])` | Timestamped readings. |
| `verifications` | `mapping(uint256 => Verification[])` | Physical-work confirmations. |
| `bondDistributions` | `mapping(uint256 => Distribution[])` | Settlement records. |
| `earthFundBalance` | `uint256` | Accumulated 10 % earth-fund share. |
| `GREENWASHING_VERIFICATION_THRESHOLD` | `uint256` constant = `50` | % physical verifications below which penalty applies. |

---

### Structs

**`Bond`**
```
bondId          uint256
regenerator     address  — who performs the physical work
landowner       address  — land rights holder
landDescription string
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`EcologicalMetrics`**
```
timestamp               uint256
soilHealth              uint256  — 0-10000
biodiversity            uint256  — 0-10000
carbonSequestration     uint256  — 0-10000
waterQuality            uint256  — 0-10000
ecosystemResilience     uint256  — 0-10000
submitter               address
physicalWorkVerified    bool     — was in-person work confirmed?
```

**`Verification`**
```
timestamp           uint256
verifier            address
confirmsPhysicalWork bool
notes               string
localResident       bool  — is verifier local to the land?
```

---

### Functions

#### `createBond(regenerator, landowner, landDescription)`
- `payable`; only owner can call (unusual — restricts bond creation to contract owner)
- Stores bond; emits `BondCreated`

#### `submitMetrics(bondId, soil, biodiversity, carbon, water, resilience, physicalWorkVerified)`
- Only `bonds[bondId].regenerator`
- Appends `EcologicalMetrics`

#### `addVerification(bondId, confirmsPhysicalWork, notes, localResident)`
- Open to anyone
- Appends `Verification`

#### `regenerationScore(bondId) → uint256`
Average of the five ecological metrics from the latest reading:
```
score = (soilHealth + biodiversity + carbonSequestration + waterQuality + ecosystemResilience) / 5
```
Returns 5 000 if no metrics.

#### `greenwashingDetected(bondId) → bool`
```
physicalCount = count of verifications where confirmsPhysicalWork = true
physicalPercent = physicalCount * 100 / total
return physicalPercent < GREENWASHING_VERIFICATION_THRESHOLD  (50%)
```
Returns false if no verifications exist (benefit of the doubt given to new bonds).

#### `calculateBondValue(bondId) → uint256`
```
bondValue = stakeAmount × avgScore / 5000
```
Where `avgScore` is `regenerationScore(bondId)`. This is the simplified V2 formula — V1 used a multi-factor product with physical-work and local-verification multipliers.

#### `distributeBond(bondId)`
- `nonReentrant`, `whenNotPaused`
- Checks `DISTRIBUTION_TIMELOCK`
- Checks `_requireMissionCompliance()` (OPEN_SOURCE, PRIVACY_DEFAULT)
- Detects greenwashing
- On **no greenwashing:** appreciation split 50 % regenerator / 40 % landowner / 10 % earthFund
- On **greenwashing:** 0 % regenerator / 100 % landowner
- Calls `_useYieldPool` for payouts
- Emits `BondDistributed` or `GreenwashingPenalty`

---

### Math Summary

| Computation | Formula |
|---|---|
| Regeneration score | `(soil + bio + carbon + water + resilience) / 5` |
| Bond value | `stake × avgScore / 5000` |
| Greenwashing threshold | `physicalVerif% < 50` |
| Normal split | 50 / 40 / 10 regenerator / landowner / fund |
| Greenwashing split | 0 / 100 / 0 |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, regenerator, landowner, stakeAmount)` | `createBond` |
| `MetricsSubmitted(bondId, timestamp)` | `submitMetrics` |
| `VerificationAdded(bondId, verifier)` | `addVerification` |
| `BondDistributed(bondId, regeneratorShare, landownerShare, earthFundShare)` | `distributeBond` |
| `GreenwashingPenalty(bondId)` | `distributeBond` when penalty fires |

---

### Design Intent

Make landowners financially motivated to hire genuine regenerators, while regenerators earn appreciation only for physically verified ecological improvements. The 10 % earth fund accumulates protocol-level capital for further regeneration grants.

### Why It Didn't Make Production

1. **Mission principle mismatch** — checking `OPEN_SOURCE_VERIFIABLE` and `PRIVACY_DEFAULT` for a land-stewardship bond is semantically wrong; these were copy-pasted from a builder contract template.
2. **Owner-only bond creation** — restricts network growth; should be open to any regenerator/landowner pair.
3. **Simplified V2 formula** — the `stake × score / 5000` formula removed the physical-work and local-verification multipliers that gave V1 its nuance; a subsequent version restored them properly.
4. **Yield pool dependency** — appreciation payouts require the yield pool to be funded externally; no automated replenishment existed.

---

---

# 5. CommonGroundBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/CommonGroundBonds.sol`
**Lines:** ~525
**Inherits:** `ReentrancyGuard`, `MissionEnforcement` (standalone)

## What It Was Supposed to Do

A two-person bond for bridge-building across political or social divides. Both parties stake ETH. The bond appreciates when external witnesses confirm that the bridge-builders have genuinely understood each other, collaborated, and respected one another. A division penalty applies if respect or bridge quality falls below thresholds.

## Why It Was Likely Deprecated

Uses the deprecated `payable().transfer()` ETH dispatch pattern throughout, which was the primary reason for retirement and replacement by `CommonGroundBondsV2`. The five-dimensional multiplier stack also produced wildly variable bond values making the contract difficult to audit.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma, ReentrancyGuard + MissionEnforcement imports
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `bridgeMetrics` | `mapping(uint256 => BridgeMetrics[])` | Metric history. |
| `witnesses` | `mapping(uint256 => Witness[])` | Cross-community witness verifications. |
| `totalRippleEffect` | `mapping(uint256 => uint256)` | Cumulative ripple effect counter. |
| `communityBridgeFund` | `uint256` | Accumulated community share. |

---

### Structs

**`Bond`**
```
bondId          uint256
builderA        address
builderB        address
communityA      string  — what community/side is builderA from?
communityB      string
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`BridgeMetrics`**
```
timestamp           uint256
submitter           address
understandingQuality uint256  — 0-10000
collaborationScore  uint256  — 0-10000
respectLevel        uint256  — 0-10000
rippleEffect        uint256  — how many others were impacted?
notes               string
```

**`Witness`**
```
timestamp           uint256
witness             address
fromCommunityA      bool  — is witness from same side as builderA?
confirmsUnderstanding bool
confirmsRespect     bool
notes               string
```

---

### Functions

#### `createBond(builderB, communityA, communityB)`
- `payable`; requires `msg.value > 0`
- `builderB` must be a different address from `msg.sender`
- Sets `builderA = msg.sender`
- Emits `BondCreated`

#### `submitBridgeMetrics(bondId, understandingQuality, collaborationScore, respectLevel, rippleEffect, notes)`
- Only `builderA` or `builderB`
- Appends metrics; adds `rippleEffect` to `totalRippleEffect[bondId]`

#### `addWitness(bondId, fromCommunityA, confirmsUnderstanding, confirmsRespect, notes)`
- Open to anyone (intended for community members)
- Appends witness record

#### `bridgeQualityScore(bondId) → uint256`
Average of the last 10 `BridgeMetrics` entries:
```
score = (sum of avgMetric per recent entry) / count
where avgMetric = (understanding + collaboration + respect) / 3
```
Returns 5 000 if no metrics.

#### `rippleEffectMultiplier(bondId) → uint256`
```
total = totalRippleEffect[bondId]
multiplier = 100 + total × 10
return min(multiplier, 300)   (capped at 3.0×)
```
More people impacted by the bridge = higher multiplier, capped at 300 %.

#### `dualPartyMultiplier(bondId) → uint256`
Counts recent witnesses from each community side:
- Both sides represented → 100–150 based on balance
- One side only → 50–100 based on count

#### `crossCommunityWitnessMultiplier(bondId) → uint256`
Based on confirmation rates across recent witnesses:
```
avgRate = (understandingConfirmations + respectConfirmations) / (2 × totalWitnesses) × 100
≥ 80 % → 130
≥ 50 % → 100
else   → 70 + avgRate / 4
```

#### `timeMultiplier(bondId) → uint256`
```
years = age / 31_536_000
years < 1 → 100
years 1–5 → 100 + years × 30
years > 5 → 250
```

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × quality × ripple × dualParty × witnesses × time) / 10_000_000_000
```
Five multipliers multiplied together before dividing. The 10 000 000 000 divisor is a rough attempt to normalise five 100–300 range values back to ETH scale. Due to the multiplicative compounding this is numerically unstable across extreme input combinations.

#### `shouldApplyDivisionPenalty(bondId) → bool`
```
latest quality < 4000  OR  latest respectLevel < 3000  → true
```

#### `distributeBond(bondId)`
- `nonReentrant`
- Calls `_requireMissionCompliance()`
- If **no penalty:** 60 % to bridge builders (30/30 each), 40 % to `communityBridgeFund`
- If **penalty:** 50 % refunded to each builder, 0 % to community fund
- Uses `payable().transfer()` — **deprecated**
- Emits `BondDistributed` or `DivisionPenalty`

---

### Math Summary

| Computation | Formula |
|---|---|
| Bridge quality score | Average of (understanding + collaboration + respect)/3 across last 10 readings |
| Ripple multiplier | `100 + ripple × 10`, capped 300 |
| Dual party multiplier | 50–150 based on witness community balance |
| Cross-community witness multiplier | 70–130 based on confirmation rates |
| Time multiplier | 100–250 over 5 years |
| Bond value | `stake × quality × ripple × dual × witnesses × time / 10_000_000_000` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, builderA, builderB)` | `createBond` |
| `MetricsSubmitted(bondId)` | `submitBridgeMetrics` |
| `WitnessAdded(bondId, witness)` | `addWitness` |
| `BondDistributed(bondId, builderAShare, builderBShare, communityShare)` | `distributeBond` |
| `DivisionPenalty(bondId)` | `distributeBond` when penalty fires |

---

### Design Intent

Create an economic incentive for politically or culturally opposed individuals to actively build bridges rather than entrench division. Witnesses from both communities provide accountability. Long-duration bonds with wide ripple effects produce the highest yields.

### Why It Didn't Make Production

1. **`payable().transfer()` deprecated pattern** — replaced by `.call{}` in V2.
2. **Numerically unstable multiplier stack** — five compounding 100–300-range multipliers with a fixed divisor is difficult to calibrate and produces extreme outliers.
3. **No Sybil resistance on witnesses** — anyone can add a witness entry; there is no stake or identity check.
4. **Missing timelock** — no `DISTRIBUTION_TIMELOCK` check; bonds can be settled immediately after creation.
5. **Community fund has no withdrawal mechanism** — ETH in `communityBridgeFund` is permanently locked with no governance over its use.

---

---

# 6. SwapGate.sol

**Path:** `theloopbreaker-site/contracts/deprecated/SwapGate.sol`
**Lines:** 99
**Inherits:** `Ownable`

## What It Was Supposed to Do

A token swap gateway allowing holders of Vaultfire tokens to exchange them for ETH, USDC, or SOL. A 0.10 % fee is charged on each swap. KYC is enforced by default; addresses on the `trustedID` list bypass the KYC gate.

## Why It Was Likely Deprecated

`swapToSOL` is a complete stub — it collects the fee, pulls tokens, and emits an event, but performs no cross-chain bridging. The 1:1 exchange rate is also hardcoded with no price discovery, making the contract unusable in practice without manual ETH/USDC pre-funding.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
L4    import "@openzeppelin/contracts/access/Ownable.sol";
```

```
L12   IERC20 public immutable vaultfire;
L13   IERC20 public immutable usdc;
```
Both token addresses are set at construction and cannot be changed. This was intentional — prevents owner from redirecting swaps to malicious tokens.

```
L14   address public feeRecipient;
L15   uint256 public feeBps = 10;   // 0.10 %
L16   bool public kycRequired = true;
L17   mapping(address => bool) public trustedID;
```
`feeBps` is capped at 100 (1 %) by `setFee`. `kycRequired` can be toggled off by owner for testing or open deployments.

```
L27–L33 constructor(IERC20 _vaultfire, IERC20 _usdc, address _feeRecipient)
```
Validates non-zero addresses for all three parameters. Sets `Ownable` to `msg.sender`.

```
L35–L39 modifier checkKYC()
```
If `kycRequired` is true and `msg.sender` is not in `trustedID`, reverts with `"kyc required"`. This is a simple allowlist gate — no actual KYC verification occurs on-chain.

```
L42    receive() external payable {}
```
Enables the contract to receive ETH (needed to hold the ETH reserve for `swapToETH`).

---

### Admin Functions

#### `setFee(newFeeBps)`
- `onlyOwner`
- Requires `newFeeBps <= 100` (max 1 %)
- Updates `feeBps`

#### `setTrustedID(user, trusted)`
- `onlyOwner`
- Grants or revokes KYC bypass

#### `depositVaultfire(amount)` / `depositUSDC(amount)`
- `onlyOwner`
- Pulls tokens from owner's wallet into the contract reserve

---

### Swap Functions

#### `swapToETH(amount)`
```
L73   fee = amount × feeBps / 10000
L74   vaultfire.transferFrom(sender, feeRecipient, fee)         — fee portion to fee recipient
L75   vaultfire.transferFrom(sender, address(this), amount-fee) — net amount into contract
L77   ethAmount = amount - fee                                  — 1:1 minus fee
L78   require(address(this).balance >= ethAmount)               — must have ETH reserves
L79   payable(msg.sender).transfer(ethAmount)                   — send ETH, deprecated pattern
```
Assumption: 1 Vaultfire token = 1 ETH wei. There is no oracle or price feed. In practice this means the owner must pre-fund the contract with ETH equal to anticipated swap volume.

#### `swapToUSDC(amount)`
Similar to `swapToETH` but sends USDC rather than ETH. Same 1:1 ratio assumption.

#### `swapToSOL(amount, solanaAddress)`
```
L93   fee = amount × feeBps / 10000
L94   vaultfire.transferFrom(sender, feeRecipient, fee)
L95   vaultfire.transferFrom(sender, address(this), amount-fee)
L97   emit SwapToSol(msg.sender, solanaAddress, amount)
      — NO cross-chain transfer occurs
```
`solanaAddress` is a `bytes32` Solana public key stored in the event for off-chain bridge processing. The contract itself has no bridge; a listening service would need to detect the event and send SOL on the Solana side. That service was never built.

---

### Math Summary

| Computation | Formula |
|---|---|
| Fee amount | `amount × feeBps / 10000` |
| Net out amount | `amount − fee` |
| Exchange rate | 1:1 (Vaultfire:ETH/USDC/SOL), no oracle |

---

### Events

| Event | Trigger |
|---|---|
| `FeeRecipientSet(recipient)` | `setFeeRecipient` |
| `FeeUpdated(feeBps)` | `setFee` |
| `TrustedIDSet(user, trusted)` | `setTrustedID` |
| `KycRequiredSet(required)` | `setKycRequired` |
| `Swap(user, asset, amountIn, amountOut)` | `swapToETH`, `swapToUSDC` |
| `SwapToSol(user, solanaAddress, amountIn)` | `swapToSOL` |

---

### Design Intent

A lightweight swap entry point allowing Vaultfire ecosystem participants to convert their tokens to widely accepted value forms. The KYC gate was included to comply with anticipated regulatory requirements without implementing full on-chain identity verification.

### Why It Didn't Make Production

1. **`swapToSOL` is a stub** — no cross-chain execution; the off-chain bridge service was never built.
2. **Hardcoded 1:1 rate** — no price oracle; economically non-functional.
3. **Reserve pre-funding required** — owner must manually maintain ETH/USDC reserves; no AMM or automated liquidity.
4. **`.transfer()` in `swapToETH`** — deprecated pattern; should use `.call{}`.
5. **KYC is an allowlist, not verification** — `trustedID` is manually managed by owner; does not integrate with any identity provider.

---

---

# 7. BuilderBeliefBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/BuilderBeliefBondsV2.sol`
**Lines:** ~576
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

V2 of the builder belief bond. Stakers back software builders across three tiers (Supporter / Believer / Champion) with vesting periods from 90 to 365 days. The bond appreciates when builders demonstrably build (code commits, deployments, users served) rather than transact (high token volume penalises the score). The `_useYieldPool` path handles appreciation payouts; a timelock prevents instant distribution.

## Why It Was Likely Deprecated

The divisor was changed from V1's 100 000 000 to 50 000 000 as part of the 2026-01-07 math fix, but the community verification logic was not redesigned alongside — verification data from a small or absent community defaulted to 100 (neutral) in ways that masked poor builder activity. Superseded by a version with mandatory minimum verifications.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L3   SPDX, pragma ^0.8.25, import BaseYieldPoolBond
```

---

### Enums

**`BeliefTier`**
```
Supporter  = 0   (< 0.001 ETH stake, 90-day vest)
Believer   = 1   (0.001–0.01 ETH, 180-day vest)
Champion   = 2   (> 0.01 ETH, 365-day vest)
```

---

### State Variables

Same structure as V1 but includes `yieldPool` via `BaseYieldPoolBond`.

| Variable | Type | Purpose |
|---|---|---|
| `BELIEVER_THRESHOLD` | `uint256` constant = `0.001 ether` | Minimum stake for Believer tier. |
| `CHAMPION_THRESHOLD` | `uint256` constant = `0.01 ether` | Minimum stake for Champion tier. |
| `SUPPORTER_VESTING` | `uint256` constant = `7 776 000` | ~90 days in seconds. |
| `BELIEVER_VESTING` | `uint256` constant = `15 552 000` | ~180 days. |
| `CHAMPION_VESTING` | `uint256` constant = `31 536 000` | ~365 days. |
| `BUILDING_THRESHOLD` | `uint256` constant = `4000` | Building score below this = transacting penalty. |
| `TRANSACTION_PENALTY` | `uint256` constant = `5000` | Transaction volume above this = score reduction. |

---

### Functions

#### `createBond(builder, builderName, projectDescription)`
- `payable`, `whenNotPaused`
- Assigns tier and vesting based on `msg.value`:
  - `< BELIEVER_THRESHOLD` → Supporter, 90-day vest
  - `BELIEVER_THRESHOLD ≤ value < CHAMPION_THRESHOLD` → Believer, 180-day vest
  - `≥ CHAMPION_THRESHOLD` → Champion, 365-day vest
- Stores bond; emits `BondCreated`, `TierAchieved`

#### `submitBuildingMetrics(bondId, commits, deployments, users, openSource, innovation, transactionVolume, notes)`
- `bondExists` modifier
- No restriction on submitter — builder or community member can submit
- Appends `BuildingMetrics`

#### `addCommunityVerification(bondId, confirmsBuilding, confirmsUseful, confirmsOpenSource, relationship, notes)`
- Open to anyone
- Appends `CommunityVerification`

#### `buildingScore(bondId) → uint256`
Reads the **latest** `BuildingMetrics` entry and applies weighted scoring:
```
commitScore  = min(commits × 100, 10000)       — capped at 100 commits
deployScore  = min(deploys × 1000, 10000)      — capped at 10 deployments
userScore    = min(users × 10, 10000)           — capped at 1000 users

score = (commitScore × 30 + deployScore × 30 + userScore × 20
       + openSourceScore × 10 + innovationScore × 10) / 100
```

#### `buildingVsTransacting(bondId) → uint256`
```
building = buildingScore(bondId)
if transactionVolume > TRANSACTION_PENALTY (5000):
    penalty = (transactionVolume - 5000) / 100
    return max(0, building - penalty)
else:
    return building
```
High transacting behaviour directly subtracts from the building score.

#### `communityVerificationMultiplier(bondId) → uint256`
Iterates verifications within the last 180 days:
```
avgRate = avg of (building%, useful%, openSource%) confirmation rates
≥ 80 % → 130
≥ 50 % → 100
else   → 70 + avgRate / 4
```
Returns 100 if no recent verifications (neutral default — this was the identified flaw).

#### `vestingMultiplier(bondId) → uint256`
```
vestedTime = min(now, vestingUntil) - createdAt
vestedPercent = vestedTime × 100 / totalVestingPeriod
multiplier = 50 + vestedPercent          (range 50–150)
```
Fully vested bond earns a 1.5× multiplier; unvested earns a 0.5× penalty.

#### `timeMultiplier(bondId) → uint256`
```
years < 1 → 100
years 1–5 → 100 + years × 37
years ≥ 5 → 250
```

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × building × vesting × time) / 50_000_000
```
Note: V2 divisor is 50 000 000 (down from 100 000 000 in V1) per the 2026-01-07 math fix.

Community verification is **not included** in `calculateBondValue` in V2 (it was included in V1 as a fourth multiplier). This was an oversight — the verification data is collected but not used in the formula.

#### `shouldApplyTransactingPenalty(bondId) → bool`
```
building = buildingVsTransacting(bondId)
return building < BUILDING_THRESHOLD  (4000)
```

#### `distributeBond(bondId)`
- `nonReentrant`, `whenNotPaused`
- Checks `DISTRIBUTION_TIMELOCK`
- Computes appreciation
- If **no penalty:** 60 % builder, 30 % stakers, 10 % builderFund
- If **transacting penalty:** 100 % to builder (odd — should penalise builder, not reward them; this appears to be a logic inversion bug from V1)
- Calls `_useYieldPool(builderShare + stakersShare)`

---

### Math Summary

| Computation | Formula |
|---|---|
| Building score | Weighted (commits 30 + deploys 30 + users 20 + openSource 10 + innovation 10) / 100 |
| Building vs transacting | `buildingScore − max(0, (txVol − 5000) / 100)` |
| Verification multiplier | 70–130 based on community confirmation rates |
| Vesting multiplier | `50 + vestedPercent` (50–150) |
| Time multiplier | 100–250 over 5 years |
| Bond value | `(stake × building × vesting × time) / 50_000_000` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, staker, builder)` | `createBond` |
| `TierAchieved(bondId, staker, tier)` | `createBond` |
| `BuildingMetricsSubmitted(bondId, submitter)` | `submitBuildingMetrics` |
| `CommunityVerificationAdded(bondId, verifier)` | `addCommunityVerification` |
| `BondDistributed(bondId, builderShare, stakersShare)` | `distributeBond` |
| `TransactingPenalty(bondId, reason)` | `distributeBond` when penalty fires |

---

### Design Intent

Align financial return with genuine building activity. Anti-flipping vesting tiers mean short-term speculators earn the least; long-term believers in real builders earn the most.

### Why It Didn't Make Production

1. **Verification not used in bond value formula** — community verification data is collected but dropped from the `calculateBondValue` formula in V2 (present in V1), an oversight.
2. **Penalty logic inversion** — the transacting penalty sends 100 % to the builder, which rewards a misbehaving builder rather than punishing them.
3. **Neutral default for missing verifications** — returning 100 for absent verification data means unverified bonds are treated the same as well-verified ones.
4. **Divisor recalibration incomplete** — while the 2026-01-07 fix changed the divisor, the formula still produces multi-ETH outputs for mid-range inputs at Champion tier stake levels.

---

---

# 8. VaultfireRewardStream.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VaultfireRewardStream.sol`
**Lines:** 127
**Inherits:** `AccessControl`

## What It Was Supposed to Do

A token-based (ERC-20, not ETH) streaming reward contract. An admin or automation service calls `streamReward(recipient, amount)` to vest tokens linearly to a recipient over a configurable `streamDuration`. Recipients can call `claim()` at any time to pull currently vested tokens. A new stream rolls any unvested remainder forward rather than discarding it.

## Why It Was Likely Deprecated

Superseded by `RewardStream.sol` which is ETH-based (fitting Vaultfire's ETH-native bond economics) and has explicit two-step timelocked admin/governor transfer. `VaultfireRewardStream` only uses ERC-20 tokens and lacks the governor-controlled multiplier update pathway needed by `RewardMultiplier.sol`.

---

## Full Source with Line-by-Line Walkthrough

```
L7    contract VaultfireRewardStream is AccessControl {
L8        bytes32 public constant AUTOMATION_ROLE = keccak256("AUTOMATION_ROLE");
```
`DEFAULT_ADMIN_ROLE` (set to deployer) and `AUTOMATION_ROLE` are the two access tiers. Both can trigger `streamReward`.

```
L10   IERC20 public immutable rewardToken;
L11   uint64 public streamDuration;
```
`rewardToken` is fixed at deploy. `streamDuration` is the default vesting window for all new streams; can be updated by admin.

```
L13–L18 struct Stream
```
```
total    uint128  — total tokens in this stream
released uint128  — tokens already disbursed
start    uint64   — stream start timestamp
duration uint64   — stream duration in seconds
```
Packed into two storage slots (128 + 128 = 256 bits; 64 + 64 = 128 bits). Space-efficient design.

```
L20   mapping(address => Stream) private streams;
```
One stream per recipient at a time. A new `streamReward` call replaces (while rolling over) the existing stream.

---

### `claimable(address recipient) → uint256`

```
L63   if stream.total == 0 or duration == 0 → return 0
L68   if block.timestamp <= stream.start → return 0
L72   elapsed = block.timestamp − stream.start
L73   if elapsed >= duration → return total − released    (fully vested)
L76   vested = (total × elapsed) / duration               (linear vesting)
L77   if vested <= released → return 0
L79   return vested − released
```
Standard linear vesting. The `if vested <= released` guard handles the case where `claimable` is called twice in the same block without a new stream.

---

### `streamReward(recipient, amount)`
- Restricted to `DEFAULT_ADMIN_ROLE` or `AUTOMATION_ROLE`
- Pulls `amount` tokens from `msg.sender` via `transferFrom` (`_pullFunding`)
- Calls `_releaseAvailable(recipient, stream)` to disburse any currently vested tokens before resetting the stream
- Calculates `remaining = stream.total − stream.released`
- Resets stream:
  ```
  stream.total    = remaining + amount   (roll-over)
  stream.released = 0
  stream.start    = now
  stream.duration = streamDuration
  ```
- Emits `StreamScheduled`

**Roll-over design:** Unvested tokens are never lost. They are folded into the new stream. The new `total` reflects the sum of old unvested remainder and the new allocation.

---

### `claim()`
- Public; no access restriction
- Calls `_releaseAvailable(msg.sender, stream)` which reads `claimable` and sends that amount via `rewardToken.transfer`

---

### Math Summary

| Computation | Formula |
|---|---|
| Vested amount | `(total × elapsed) / duration` |
| Claimable | `vested − released` (if positive) |
| Roll-over total | `remaining + newAmount` |

---

### Events

| Event | Trigger |
|---|---|
| `StreamScheduled(recipient, amount, start, duration)` | `streamReward` |
| `StreamClaimed(recipient, amount, remaining)` | `_disburse` |
| `StreamDurationUpdated(duration)` | `setStreamDuration` |
| `AutomationRoleGranted(account)` | `grantAutomationRole` |
| `AutomationRoleRevoked(account)` | `revokeAutomationRole` |

---

### Design Intent

Provide a smooth ERC-20 reward distribution pipeline that prevents cliff-vesting. Automation bots can periodically top up streams; recipients pull at their convenience.

### Why It Didn't Make Production

1. **ERC-20 only** — Vaultfire's bond economics are ETH-native; the ETH-based `RewardStream.sol` was preferred.
2. **No governor multiplier pathway** — `RewardMultiplier.sol` calls `rewardStream.updateMultiplier(user, multiplier)` but `VaultfireRewardStream` has no such function.
3. **Single stream per recipient** — if a recipient has multiple independent reward sources they all compete for the single slot and the roll-over may lose track of which source provided which tokens.
4. **`_pullFunding` uses `msg.sender`** — the caller must pre-approve the contract to spend their tokens; this is workable but adds friction for automation bots.

---

---

# 9. VerdantAnchorBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VerdantAnchorBonds.sol`
**Lines:** ~554
**Inherits:** `ReentrancyGuard`, `MissionEnforcement` (standalone V1)

## What It Was Supposed to Do

V1 of the earth-regeneration bond. Same five ecological metrics as V2 but with a richer bond-value formula incorporating physical-work and local-verification multipliers. The V1 greenwashing penalty has an inverted split compared to V2.

## Why It Was Likely Deprecated

Uses deprecated `payable().transfer()` and has the inverted greenwashing penalty split (regenerator gets stake, landowner gets 0 — the opposite of what was intended; V2 corrected to 0/100). Also lacks timelock.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L5   SPDX, pragma ^0.8.25, ReentrancyGuard + MissionEnforcement imports
```

---

### Key Differences From V2

| Feature | V1 | V2 |
|---|---|---|
| Inheritance | Standalone | BaseYieldPoolBond |
| ETH transfer | `.transfer()` | `_useYieldPool()` |
| Timelock | No | Yes (DISTRIBUTION_TIMELOCK) |
| Greenwashing penalty | Regenerator gets stake, landowner 0 (inverted) | Regenerator 0, landowner 100 % (correct) |
| Bond value formula | Multi-factor product | Simple `stake × score / 5000` |
| `whenNotPaused` | No | Yes |

---

### Functions

#### `regenerationScore(bondId) → uint256`
Only averages entries where `physicalWorkVerified = true`:
```
count = entries where physicalWorkVerified
totalScore = sum(avgEcoScore) for those entries
return count > 0 ? totalScore / count : 5000
```
This differs from V2 where all entries are averaged; V1 deliberately discards unverified readings from the score.

#### `physicalWorkMultiplier(bondId) → uint256`
```
verifiedPercent = physicalVerifiedCount × 100 / totalCount
multiplier = verifiedPercent × 1.5     (0 to 150)
```
Returns 0 for no verifications, 150 for 100 % physical work confirmation.

#### `localVerificationMultiplier(bondId) → uint256`
Counts three confirmation types from verifications flagged `localResident = true`:
- `confirmsPhysicalWork`
- `confirmsEcological` (V1 has this extra field)
- `confirmsLongTerm`

Maps to range 50–150 based on average confirmation rate across the three types.

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × regen × physicalWork × localVerif × time) / 1_000_000_000
```
Where:
- `regen` = regeneration score (0–10 000)
- `physicalWork` = 0–150
- `localVerif` = 50–150
- `time` = 100–200

Divisor 1 000 000 000 normalises the product of four multipliers.

#### `distributeBond(bondId)` — greenwashing penalty (V1 bug)
```
if greenwashing:
    // V1 intended to punish regenerator but inverted the logic
    regeneratorShare = bond.stakeAmount    (WRONG — regenerator gets ALL)
    landownerShare = 0
    payable(bond.regenerator).transfer(regeneratorShare)   // deprecated
else:
    // Normal: 50/40/10
    regeneratorShare = appreciation × 50 / 100
    landownerShare   = appreciation × 40 / 100
    earthFundShare   = appreciation × 10 / 100
    payable(bond.regenerator).transfer(regeneratorShare)   // deprecated
    payable(bond.landowner).transfer(landownerShare)       // deprecated
```
**The greenwashing penalty is inverted.** When greenwashing is detected the regenerator receives the full stake amount — the opposite of the intended punishment. V2 corrected this.

---

### Math Summary

| Computation | Formula |
|---|---|
| Regeneration score | Average eco scores from physically-verified entries only |
| Physical work multiplier | `verifiedPercent × 1.5` (0–150) |
| Local verification multiplier | 50–150 based on local resident confirmation rates |
| Bond value | `(stake × regen × physicalWork × localVerif × time) / 1_000_000_000` |
| Normal split | 50 / 40 / 10 regenerator / landowner / earth fund |
| Greenwashing split (bug) | 100 / 0 / 0 regenerator / landowner / earth fund |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, regenerator, landowner, stake)` | `createBond` |
| `MetricsSubmitted(bondId, timestamp)` | `submitMetrics` |
| `VerificationAdded(bondId, verifier)` | `addVerification` |
| `BondDistributed(bondId, regen, landowner, earth)` | `distributeBond` |
| `GreenwashingPenalty(bondId)` | When penalty fires (with incorrect split) |

---

### Design Intent

Reward demonstrably physical ecological restoration backed by local community verification. The physical-work-only score filter was the core innovation — only metrics tied to actual ground-truthed work count.

### Why It Didn't Make Production

1. **Greenwashing penalty is inverted** — a critical logic bug rewards the bad actor.
2. **`payable().transfer()` deprecated** — all three transfer calls use the old pattern.
3. **No timelock on distribution** — bonds can be settled immediately.
4. **No `whenNotPaused` modifier** — no emergency stop mechanism.
5. **Superseded by V2** which fixed the penalty logic but simplified the formula too aggressively (removing physical-work and local-verification multipliers).

---

---

# 10. FreedomVow.sol

**Path:** `theloopbreaker-site/contracts/deprecated/FreedomVow.sol`
**Lines:** 68
**Inherits:** `BeliefOracle`

## What It Was Supposed to Do

A one-time ignition rite layered on top of the `BeliefOracle`. A guardian submits a human-readable vow text plus a zero-knowledge attestation (`zkSig`). If the oracle returns a resonance score above 50 and global resonance has not drifted, the guardian is marked as having vowed, a starter yield of 1 token (1e18) is queued in the reward stream, and a `FreedomIgnited` event is emitted containing the vow's `keccak256` hash.

## Why It Was Likely Deprecated

Archived because the `BeliefOracle` base contract itself was being redesigned to use Dilithium-based attestations. `FreedomVow` was tightly coupled to the old oracle interface and could not be migrated without a full rewrite.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L4    import {BeliefOracle} from "../BeliefOracle.sol";
L5    import {DilithiumAttestor} from "../DilithiumAttestor.sol";
L6    import {RewardStream} from "./RewardStream.sol";
```
All three imports are active (non-deprecated) production contracts. `FreedomVow` is the deprecated layer.

```
L8    contract FreedomVow is BeliefOracle {
```
Extends `BeliefOracle`. Inherits `queryBelief(vow, zkSig)`, `resonanceDrifted`, and the `rewardStream` reference.

```
L9    uint256 public constant STARTER_YIELD = 1e18;
```
1 token (denominated in 18-decimal units). This is the reward queued for each guardian on first ignition.

```
L11   uint256 private _guardianCount;
L13   mapping(address => bool) public vowed;
```
`_guardianCount` counts total ignitions (view via `freeGuardiansCount()`). `vowed` is a per-address permanent flag — once set it cannot be unset by the guardian (only through a contract upgrade).

```
L15–L19 custom errors
```
```
AlreadyVowed()      — guardian has already completed the rite
ResonanceDrifted()  — global oracle resonance is in a drift state
ResonanceTooLow()   — guardian's individual resonance score ≤ 50
```
Custom errors save gas vs string revert messages.

---

### `igniteFreedom(vow, zkSig) → resonance`

```
L39   address guardianAccount = msg.sender;
L40   if (vowed[guardianAccount]) revert AlreadyVowed();
```
Strict one-per-address gate. No batch vows.

```
L43   if (resonanceDrifted) revert ResonanceDrifted();
```
`resonanceDrifted` is a state variable in `BeliefOracle` set by the oracle admin when the system's collective belief signal is unstable. When drifted, no new vows can be ignited — the system is in a healing state.

```
L46   resonance = queryBelief(vow, zkSig);
```
Calls the parent `BeliefOracle.queryBelief` which validates the `zkSig` attestation and returns the guardian's resonance score. The vow text itself is hashed (not stored on-chain) in the emitted event.

```
L47   if (resonance <= 50) revert ResonanceTooLow();
```
Resonance must be above the midpoint. A score of exactly 50 is rejected.

```
L52   vowed[guardianAccount] = true;
L53   unchecked { _guardianCount += 1; }
```
State changes before the external interaction (correct CEI pattern). `unchecked` is safe because `_guardianCount` overflowing `uint256` in practice is not a concern.

```
L57   rewardStream.queueRewards(guardianAccount, STARTER_YIELD);
```
External interaction last (CEI-correct). Queues — does not immediately transfer — the starter yield. The guardian must later call `claimRewards` on the `RewardStream`.

```
L59   emit FreedomIgnited(guardianAccount, keccak256(bytes(vow)), resonance, STARTER_YIELD);
```
The vow's content is hashed in the event so the event log serves as a permanent commitment record without storing the full text on-chain.

---

### `freeGuardiansCount() → uint256`
Simple view returning `_guardianCount`. External systems can watch this number to assess community growth.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `STARTER_YIELD` | `uint256` constant = `1e18` | Reward queued per ignition. |
| `_guardianCount` | `uint256` private | Total ignitions. |
| `vowed` | `mapping(address → bool)` | Permanent vow record per guardian. |

### Events

| Event | Trigger |
|---|---|
| `FreedomIgnited(guardian, vowHash, resonance, starterYield)` | `igniteFreedom` |

---

### Design Intent

A lightweight ceremony contract that marks a guardian's entry into the Vaultfire belief system with an irreversible on-chain commitment. The ZK attestation ensures the vow is backed by verifiable belief state, not just a transaction.

### Why It Didn't Make Production

1. **Tightly coupled to old `BeliefOracle` interface** — the oracle redesign broke the inheritance chain.
2. **Vow is permanent** — no mechanism for a guardian to reconsider or a governance process to invalidate a vow in edge cases.
3. **Starter yield is flat** — every guardian receives exactly 1 token regardless of resonance score; a resonance-proportional reward was proposed but not implemented here.
4. **One contract, one rite** — the pattern was generalised into a composable rite system in production.

---

---

# 11. HealthCommonsBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/HealthCommonsBondsV2.sol`
**Lines:** ~532
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

A company stakes ETH on pollution reduction and health outcome improvement for a specific community. Two independent data streams — `PollutionMetrics` and `HealthOutcomes` — are submitted over time. The bond appreciates when both pollution decreases and health improves, as measured against baseline (first readings). A poisoning penalty fires if either stream falls below threshold.

## Why It Was Likely Deprecated

The divisor 100 000 000 in V2 (10× larger than the V1 divisor of 1 000 000) drastically reduced bond values; at mid-range scores a 1 ETH stake would appreciate to fractions of a wei. This overcorrection from the V1 uint256-underflow bug (see contract 33) made V2 economically inert. A subsequent version recalibrated the divisor.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma, BaseYieldPoolBond import
```

---

### Structs

**`Bond`**
```
bondId          uint256
company         address
communityName   string
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`PollutionMetrics`**
```
timestamp       uint256
airQuality      uint256  — 0-10000 (10000 = pristine)
waterQuality    uint256  — 0-10000
foodSafety      uint256  — 0-10000
submitter       address
```

**`HealthOutcomes`**
```
timestamp       uint256
lifeExpectancy  uint256  — 0-10000
cancerRate      uint256  — 0-10000 (10000 = zero cancer)
respiratoryHealth uint256
mentalHealth    uint256
childDevelopment uint256
populationCount uint256  — required for statistical validity
submitter       address
```

**`CommunityVerification`**
```
timestamp           uint256
verifier            address
confirmsImprovement bool
confirmsPhysical    bool
residencyYears      uint256
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `POISONING_POLLUTION_THRESHOLD` | `uint256` = `80` | Pollution score below this = penalty. |
| `POISONING_HEALTH_THRESHOLD` | `uint256` = `80` | Health score below this = penalty. |
| `POISONING_COMMUNITY_THRESHOLD` | `uint256` = `70` | Community verification below this = penalty. |

---

### Functions

#### `submitPollutionMetrics(bondId, air, water, food)`
- Only company
- Requires `populationCount > 0` constraint only on `HealthOutcomes` (not applicable here)
- Appends to `pollutionMetrics[bondId]`

#### `submitHealthOutcomes(bondId, life, cancer, respiratory, mental, child, population)`
- Only company
- Requires `population > 0` — prevents statistically meaningless submissions
- Appends to `healthOutcomes[bondId]`

#### `addCommunityVerification(bondId, confirmsImprovement, confirmsPhysical, residencyYears)`
- Open to anyone
- `residencyYears` adds weight to long-term residents

#### `pollutionReductionScore(bondId) → uint256`
Compares first reading vs latest reading:
```
firstAvg  = (first.air + first.water + first.food) / 3
latestAvg = (latest.air + latest.water + latest.food) / 3
improvement = int256(latestAvg) - int256(firstAvg)     // may be negative

if improvement >= 0:
    return min(10000, 5000 + improvement / 2)
else:
    return improvement > -4000 ? uint256(50 + int256(5000) + improvement / 80) : 0
```
Returns 5 000 (neutral) if fewer than 2 readings. Capped at 10 000.

#### `healthImprovementScore(bondId) → uint256`
Same pattern as `pollutionReductionScore` but averaging five health dimensions.

#### `communityVerificationMultiplier(bondId) → uint256`
```
verifiedCount = count where confirmsImprovement = true
total = all verifications
verifiedPct = verifiedCount × 100 / total

if verifiedPct >= 70: return 150
if verifiedPct >= 40: return 100
else:                 return 50 + verifiedPct
```
Returns 100 if no verifications.

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × pollution × health × community × time) / 100_000_000
```
The 100 000 000 divisor was the over-correction that caused near-zero outputs.

#### `shouldApplyPoisoningPenalty(bondId) → (bool, string)`
```
pollution < 80  → (true, "Pollution causing harm")
health < 80     → (true, "Health outcomes worsening")
community < 70  → (true, "Community not seeing improvements")
```

#### `distributeBond(bondId)`
- Requires at least one `HealthOutcomes` entry with population > 0 before distribution can proceed
- 70 % community / 30 % company on normal appreciation
- 100 % community if penalty fires
- Uses `_useYieldPool`

---

### Math Summary

| Computation | Formula |
|---|---|
| Pollution reduction score | Delta between first/latest reading, mapped to 0–10 000 |
| Health improvement score | Same pattern across five health dimensions |
| Community verification multiplier | 50–150 based on confirmation percentage |
| Time multiplier | Inherited from BaseYieldPoolBond |
| Bond value | `(stake × pollution × health × community × time) / 100_000_000` |
| Normal split | 70 / 30 community / company |
| Penalty split | 100 / 0 |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, company, stake)` | `createBond` |
| `PollutionMetricsSubmitted(bondId)` | `submitPollutionMetrics` |
| `HealthOutcomesSubmitted(bondId)` | `submitHealthOutcomes` |
| `CommunityVerificationAdded(bondId, verifier)` | `addCommunityVerification` |
| `BondDistributed(bondId, communityShare, companyShare)` | `distributeBond` |
| `PoisoningPenalty(bondId, reason)` | `distributeBond` when penalty fires |

---

### Design Intent

Hold companies financially accountable for the health consequences of their pollution. The dual data stream — pollution AND health — ensures companies cannot claim credit for population improvements unrelated to their pollution levels.

### Why It Didn't Make Production

1. **Divisor overcorrection** — 100 000 000 is too large; bond values near zero at realistic inputs.
2. **Company self-reports both data streams** — no independent oracle; a company can submit favourable metrics unilaterally.
3. **Population requirement only on health, not pollution** — asymmetric validation between the two streams.
4. **Superseded** by a version incorporating an independent health-data oracle and a recalibrated divisor.

---

---

# 12. CompetitiveIntegrityBond.sol

**Path:** `theloopbreaker-site/contracts/deprecated/CompetitiveIntegrityBond.sol`
**Lines:** ~628
**Inherits:** `ReentrancyGuard` (standalone)

## What It Was Supposed to Do

Sports team bonds that incentivise competitive integrity across a full season. A team stakes a minimum of 1 ETH. At season end, three score sources — effort metrics, anti-tanking metrics, and fan consensus — combine into an `IntegrityLevel` enum (Tanking / Questionable / Competitive / Elite). Each level determines a different appreciation percentage and distribution split between team, league, and fans.

## Why It Was Likely Deprecated

`_calculateFanConsensusScore` always returns the hardcoded value 5 000 — the fan consensus scoring layer was never implemented. The contract cannot function correctly without it.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L3   SPDX, pragma ^0.8.25, ReentrancyGuard import
```

---

### Enums

**`IntegrityLevel`**
```
Tanking      = 0
Questionable = 1
Competitive  = 2
Elite        = 3
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `MIN_STAKE` | `uint256` constant = `1 ether` | Minimum team stake. |
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `effortMetrics` | `mapping(uint256 => EffortMetrics[])` | Game-by-game effort readings. |
| `tankingMetrics` | `mapping(uint256 => TankingMetrics[])` | Anti-tanking readings. |
| `fanCompensationPool` | `mapping(uint256 => uint256)` | Held fan shares before claim. |
| `yieldPool` | `uint256` | Externally funded appreciation pool. |

---

### Structs

**`Bond`**
```
bondId          uint256
team            address
teamName        string
leagueAddress   address
stakeAmount     uint256
seasonStartDate uint256
seasonEndDate   uint256
active          bool
```

**`EffortMetrics`**
```
timestamp           uint256
hustleRating        uint256  — 0-10000
fourthQuarterEffort uint256  — effort in closing minutes
backToBackPerformance uint256 — on second game of back-to-back nights
bigGameEffort       uint256  — performance in high-stakes games
```

**`TankingMetrics`**
```
timestamp               uint256
winProbabilityDelta     int256  — positive = team improved chances
suspiciousInjuries      uint256 — count of suspicious DNPs
quitScore               uint256 — 0-10000 (0 = never quit)
performanceTrajectory   int256  — season trend direction
```

---

### Functions

#### `createBond(teamName, leagueAddress, seasonStart, seasonEnd)`
- `payable`; requires `msg.value >= MIN_STAKE (1 ETH)`
- `seasonEnd > seasonStart` required
- Emits `BondCreated`

#### `submitEffortMetrics(bondId, hustle, q4, b2b, bigGame)`
- Only `bonds[bondId].team`
- Appends `EffortMetrics`

#### `submitTankingMetrics(bondId, winProbDelta, suspiciousInjuries, quitScore, trajectory)`
- Only team
- Appends `TankingMetrics`

#### `_calculateEffortScore(bondId) → uint256`
Average of the four effort dimensions across all submitted readings:
```
for each entry:
    entryScore = (hustle + q4 + b2b + bigGame) / 4
total = sum(entryScore)
return count > 0 ? total / count : 5000
```

#### `_calculateAntiTankingScore(bondId) → uint256`
Inverts tanking signals — higher tanking indicators produce lower scores:
```
for each TankingMetrics entry:
    — winProbDelta: positive delta is good; maps −10000..+10000 → 0..10000
    — suspiciousInjuries: each injury subtracts 500, min 0
    — quitScore: inverted (10000 − quitScore)
    — trajectory: positive = good; similar mapping
    entryScore = average of four inverted metrics
return average across all entries
```

#### `_calculateFanConsensusScore(bondId) → uint256`
```
return 5000;   // STUB — always returns neutral
```
**This function is not implemented.** It was intended to aggregate fan sentiment via an external oracle or verified fan votes. The stub makes the fan consensus dimension permanently neutral, which skews `_calculateIntegrityScore` toward the midpoint regardless of actual fan perceptions.

#### `_calculateIntegrityScore(bondId) → uint256`
```
effort = _calculateEffortScore(bondId)
antiTanking = _calculateAntiTankingScore(bondId)
fanConsensus = _calculateFanConsensusScore(bondId)   // always 5000

return (effort + antiTanking + fanConsensus) / 3
```
Because `fanConsensus` is always 5 000, the effective formula is `(effort + antiTanking + 5000) / 3`, which clamps the combined score closer to neutral than the effort and anti-tanking signals alone would produce.

#### `_determineIntegrityLevel(score) → IntegrityLevel`
```
score < 3000  → Tanking
score < 5000  → Questionable
score < 7500  → Competitive
else          → Elite
```

#### `calculateBondValue(bondId) → uint256`
Uses `IntegrityLevel` to determine the appreciation percentage applied to the `yieldPool`:
```
Tanking      → 0   % appreciation
Questionable → 10  % appreciation
Competitive  → 50  % appreciation
Elite        → 100 % appreciation
```

#### `distributeBond(bondId)`
- Requires `block.timestamp >= bond.seasonEndDate`
- `nonReentrant`
- Computes integrity level and appreciation
- Distribution splits:

| Level | Team | League | Fan Pool |
|---|---|---|---|
| Tanking | 0 % | 0 % | 100 % |
| Questionable | 20 % | 10 % | 70 % |
| Competitive | 40 % | 30 % | 30 % |
| Elite | 50 % | 30 % | 20 % |

- Fan share accumulated in `fanCompensationPool[bondId]` for individual fan claims
- League and team shares transferred immediately via `.call{}` (note: this contract mixes patterns — some transfers use call, others may use transfer in helper functions)

#### `claimFanCompensation(bondId, fanAddress)`
- Allows fans to claim their share from `fanCompensationPool`
- No fan registration mechanism exists — the pool is simply divided equally among all verified fans, but there is no on-chain fan registry; the logic stubs this with a placeholder

---

### Math Summary

| Computation | Formula |
|---|---|
| Effort score | Average of (hustle + q4 + b2b + bigGame) / 4 across all readings |
| Anti-tanking score | Inverted tanking signals, averaged |
| Fan consensus score | Hardcoded 5 000 (stub) |
| Integrity score | `(effort + antiTanking + fanConsensus) / 3` |
| Bond value | `stakeAmount + yieldPool × appreciationPct / 100` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, team, stake, seasonEnd)` | `createBond` |
| `EffortSubmitted(bondId)` | `submitEffortMetrics` |
| `TankingSubmitted(bondId)` | `submitTankingMetrics` |
| `BondDistributed(bondId, level, teamShare, leagueShare, fanShare)` | `distributeBond` |

---

### Design Intent

Create financial accountability for professional sports teams to compete honestly throughout the season. The fan compensation pool acknowledges that tank-season fans who purchased tickets deserved better value for their money. The yield pool funded by the league or sponsors provides the appreciation source.

### Why It Didn't Make Production

1. **Fan consensus score is a stub** — permanently returns 5 000; the most important stakeholder perspective is absent.
2. **No fan registry** — the `claimFanCompensation` function cannot distribute to actual fans without a verified fan database.
3. **Single team self-reports effort metrics** — trivially gameable with perfect hustle scores.
4. **No external oracle** — tanking detection based on self-reported metrics is meaningless.
5. **Yield pool must be manually funded** — no automated mechanism.

---

---

# 13. EscapeVelocityBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/EscapeVelocityBonds.sol`
**Lines:** ~493
**Inherits:** `ReentrancyGuard` (standalone V1)

## What It Was Supposed to Do

A bond designed for individuals trying to escape poverty. Stake is constrained to a small range (approximately $50–$500 equivalent) so it remains accessible. The bond appreciates when the staker's income grows by 150 % or more (escape velocity) above their initial income baseline. A pay-it-forward mechanism routes 20 % of appreciation to others attempting escape.

## Why It Was Likely Deprecated

Uses deprecated `.transfer()` throughout. Superseded by `EscapeVelocityBondsV2` which upgrades to `.call{}`, adds `BaseYieldPoolBond` inheritance, and simplifies the bond value formula.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma, ReentrancyGuard import
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `MIN_STAKE` | `uint256` constant = `50000000000000` (0.00005 ETH) | ~$50 at $1 000/ETH. |
| `MAX_STAKE` | `uint256` constant = `500000000000000` (0.0005 ETH) | ~$500. |
| `ESCAPE_THRESHOLD` | `uint256` constant = `15000` | 150 % income gain. |
| `RECAPTURE_THRESHOLD` | `uint256` constant = `10000` | 100 % gain — must stay above for full payout. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `progressRecords` | `mapping(uint256 => ProgressRecord[])` | Timestamped progress entries. |
| `verifications` | `mapping(uint256 => Verification[])` | Third-party verifications. |
| `payItForwardPool` | `uint256` | Pool for funding future escape attempts. |

---

### Structs

**`Bond`**
```
bondId          uint256
escaper         address
initialIncome   uint256  — baseline income at bond creation (any unit)
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`ProgressRecord`**
```
timestamp       uint256
currentIncome   uint256
autonomyScore   uint256  — 0-10000 (control over own work/life)
stabilityScore  uint256  — 0-10000 (income predictability)
peopleHelped    uint256  — others supported during escape
notes           string
submitter       address
```

**`Verification`**
```
timestamp       uint256
verifier        address
confirmsIncome  bool
confirmsGrowth  bool
relationship    string  — "employer", "accountant", "community"
```

---

### Functions

#### `createBond(initialIncome)`
- `payable`
- Requires `msg.value >= MIN_STAKE && msg.value <= MAX_STAKE`
- `initialIncome > 0` required
- Emits `BondCreated`

#### `submitProgress(bondId, currentIncome, autonomy, stability, peopleHelped, notes)`
- Only `bonds[bondId].escaper`
- Appends `ProgressRecord`

#### `addVerification(bondId, confirmsIncome, confirmsGrowth, relationship)`
- Open to anyone (intended for trusted community members or employers)
- Appends `Verification`

#### `calculateIncomeGain(bondId) → uint256`
```
latestIncome = progressRecords[bondId][last].currentIncome
return (latestIncome - initialIncome) × 10000 / initialIncome
```
Returns 0 if no records or income decreased. Units are basis points of initial income.

#### `escapeSuccessMultiplier(bondId) → uint256`
```
gain = calculateIncomeGain(bondId)
if gain >= ESCAPE_THRESHOLD (15000):
    return 150 + min(100, (gain - 15000) / 100)   — 150 to 250
if gain >= RECAPTURE_THRESHOLD (10000):
    return 100 + (gain - 10000) / 50               — 100 to 150
if gain >= 5000:
    return 50 + gain / 100                         — 50 to 100
return 50    — still trying; base multiplier
```

#### `payItForwardMultiplier(bondId) → uint256`
```
totalPeopleHelped = sum(progressRecords[bondId][*].peopleHelped)
return 100 + min(100, totalPeopleHelped × 10)       — 100 to 200
```
Helping more people during escape increases the staker's own return.

#### `verificationMultiplier(bondId) → uint256`
```
recent = verifications within last 180 days
confirmationRate = (confirmsIncome + confirmsGrowth) / (2 × recent) × 100
≥ 80 % → 130
≥ 50 % → 100
else   → 70
```

#### `timeMultiplier(bondId) → uint256`
```
years < 1 → 100
years 1–3 → 100 + years × 50
years > 3 → 200
```

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × escapeSuccess × payItForward × verification × time) / 100_000_000
```

#### `distributeBond(bondId)`
- `nonReentrant`
- Computes appreciation; requires `appreciation != 0`
- **80 % to escaper, 20 % to payItForwardPool** (regardless of whether penalty applies)
- No penalty logic in V1 — always 80/20
- Uses deprecated `payable().transfer()`
- Emits `BondDistributed`

---

### Math Summary

| Computation | Formula |
|---|---|
| Income gain | `(current − initial) × 10000 / initial` |
| Escape success multiplier | Segmented: 50–250 based on income gain ratio |
| Pay-it-forward multiplier | `100 + min(100, peopleHelped × 10)` |
| Verification multiplier | 70–130 based on confirmation rates |
| Time multiplier | 100–200 |
| Bond value | `(stake × escape × payForward × verif × time) / 100_000_000` |
| Distribution | 80 % escaper / 20 % payItForwardPool |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, escaper, initialIncome, stake)` | `createBond` |
| `ProgressSubmitted(bondId, submitter)` | `submitProgress` |
| `VerificationAdded(bondId, verifier)` | `addVerification` |
| `BondDistributed(bondId, escaperShare, payItForwardShare)` | `distributeBond` |

---

### Design Intent

Bootstrap economic escape for low-income individuals using a self-reinforcing pool. Successful escapers fund future escapers through the pay-it-forward pool. The constrained stake range prevents wealthy participants from capturing the mechanism.

### Why It Didn't Make Production

1. **Deprecated `.transfer()` pattern** — throughout distribution.
2. **No recapture penalty in V1** — if income falls back below threshold after payout, no mechanism reclaims the appreciation.
3. **Self-reported income** — no on-chain income oracle; trivially gameable.
4. **No Sybil resistance on verifications** — any address can verify.
5. **Superseded by V2** which upgrades transfer pattern and adds `BaseYieldPoolBond`.

---

---

# 14. GhostkeyAttribution.sol

**Path:** `theloopbreaker-site/contracts/deprecated/GhostkeyAttribution.sol`
**Lines:** 35
**Inherits:** None (standalone)
**Pragma:** `^0.8.19` (older than the rest of the codebase)

## What It Was Supposed to Do

Store permanent immutable contributor metadata for the Ghostkey-316 contributor and route all ETH received (or explicitly sent via `payRoyalty()`) directly to the contributor's wallet.

## Why It Was Likely Deprecated

This is a utility/provenance contract, not a bond. It was likely superseded by `CovenantFlame.sol` which serves a similar provenance-anchor role with more sophisticated ignition tracking. The `^0.8.19` pragma predates the `^0.8.25` standard used across the rest of the codebase.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.19;
```
Older pragma. Will still compile with 0.8.25 compilers but signals this was written in an earlier development phase.

```
L9–L13 string public constants
```
```
CONTRIBUTOR = "Ghostkey-316"
ENS         = "ghostkey316.eth"
PRIMARY_WALLET = "bpow20.cb.id"
GITHUB      = "github.com/Ghostkey316"
```
Immutable string constants baked into bytecode. Future forks that include this contract preserve this attribution permanently in the deployment.

```
L15   address payable public immutable royaltyRecipient;
```
Set once at construction; cannot be changed. All ETH passing through this contract goes here.

```
L17   event RoyaltyPaid(address indexed payer, uint256 amount, string token);
```
`token` is always `"ETH"` (there is no ERC-20 path); the field is included for potential future extension.

```
L19–L21 constructor(address payable recipient)
```
```
require(recipient != address(0), "recipient zero");
royaltyRecipient = recipient;
```
Single guard. No owner stored — once deployed, attribution is immutable.

```
L23–L26 receive() external payable
```
Any ETH sent directly to the contract address is immediately forwarded to `royaltyRecipient` via `.transfer()`. Emits `RoyaltyPaid`.

```
L28–L32 function payRoyalty() external payable
```
Explicit function for callers who prefer named entry points. Requires `msg.value > 0`. Same `.transfer()` forward.

**`.transfer()` risk:** The recipient is an immutable address chosen at deploy time. If `royaltyRecipient` is a contract with a complex `receive()` function, the 2300-gas stipend from `.transfer()` may be insufficient, causing silent failure. However, because the recipient is immutable and set at deploy, this is a deployment-time risk assessment rather than a post-deployment governance issue.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `CONTRIBUTOR` | `string` constant | Human-readable contributor name. |
| `ENS` | `string` constant | ENS name for contributor. |
| `PRIMARY_WALLET` | `string` constant | Coinbase-ID wallet handle. |
| `GITHUB` | `string` constant | GitHub profile URL. |
| `royaltyRecipient` | `address payable immutable` | All ETH forwarded here. |

### Events

| Event | Trigger |
|---|---|
| `RoyaltyPaid(payer, amount, token)` | `receive()` and `payRoyalty()` |

---

### Design Intent

Create a permanent, forkable attribution record ensuring the Ghostkey-316 contributor receives recognition and royalty routing across any deployment that includes this contract.

### Why It Didn't Make Production

1. **Superseded by `CovenantFlame.sol`** which provides richer provenance anchoring.
2. **Older pragma (`^0.8.19`)** — inconsistent with codebase standard of `^0.8.25`.
3. **No owner or governance** — completely immutable; cannot correct recipient address if a key is lost.
4. **`.transfer()` pattern** — deprecated.

---

---

# 15. RewardMultiplier.sol

**Path:** `theloopbreaker-site/contracts/deprecated/RewardMultiplier.sol`
**Lines:** 209
**Inherits:** None (standalone)

## What It Was Supposed to Do

Calculate reward multipliers in basis points from three inputs — a base rate, a belief score, and a loyalty score — and push the result to a connected `RewardStream` contract. On stream failure, falls back to a configured `fallbackMultiplier`.

## Why It Was Likely Deprecated

The contract calls `rewardStream.updateMultiplier(user, computed)` via try/catch, but `RewardStream.sol` (the ETH-based version) gates `updateMultiplier` behind `onlyGovernor`. The `RewardMultiplier` contract would need to be the `governorTimelock` address to call it successfully — an architectural mismatch that was never resolved.

---

## Full Source with Line-by-Line Walkthrough

```
L8–L10 interface IRewardStream
    function updateMultiplier(address user, uint256 multiplier) external;
```
Minimal interface. Any contract with this function can be plugged in.

```
L20   address public owner;
L21   IRewardStream public rewardStream;
L22   uint256 public fallbackMultiplier;    // bps, default 10 000 = 1.0×
L23   uint256 public bonusCapBps;           // default 5 000 = +0.5× max bonus
```

```
L52–L56 constructor(address stream, uint256 initialFallbackBps)
```
Sets `owner = msg.sender`, `rewardStream`, and `fallbackMultiplier` (defaults to 10 000 if 0 passed). Sets `bonusCapBps = 5 000`.

---

### `calculateMultiplier(baseRateBps, beliefScoreBps, loyaltyScoreBps) → uint256`

```
L155  base = max(baseRateBps, 10_000)                           — floor at 1.0×
L156  beliefBonus = (beliefScoreBps × bonusCapBps) / 100_000   — belief contributes up to bonusCap
L157  loyaltyBonus = loyaltyScoreBps × 5                       — each loyalty point = 5 bps
L158  computed = base + beliefBonus + loyaltyBonus
L159  maxAllowed = base + bonusCapBps
L160  return min(computed, maxAllowed)
```

**Example at default settings:**
- base = 10 000 (1.0×)
- belief = 8 000 / 100 000 × 5 000 = 400 bps (+0.04×)
- loyalty = 6 000 × 5 = 30 000 bps — this is the overflow problem: loyalty directly multiplies without a cap, so a loyalty score of 2 000 (max is 10 000) gives 10 000 bps, which is the full bonus cap by itself. The `min(computed, maxAllowed)` saves this from overflowing, but the loyalty weighting is disproportionate.

---

### `streamMultiplier(user, baseRateBps, beliefScoreBps, loyaltyScoreBps) → uint256`

```
L194  computed = calculateMultiplier(...)
L196  if rewardStream != address(0):
L197      try rewardStream.updateMultiplier(user, computed):
L198          streamed = true
L199      catch:
L200          computed = fallbackMultiplier   — revert to fallback on any failure
L202  else:
L203      computed = fallbackMultiplier
L205  emit MultiplierStreamed(user, computed, streamed)
```
The try/catch fallback means a failed stream silently returns `fallbackMultiplier` without alerting operators. An event is emitted showing `streamedOnChain = false` but there is no separate error event.

---

### Math Summary

| Computation | Formula |
|---|---|
| Base rate | `max(baseRateBps, 10_000)` |
| Belief bonus | `(beliefScore × bonusCap) / 100_000` |
| Loyalty bonus | `loyaltyScore × 5` (uncapped before final cap) |
| Computed | `base + beliefBonus + loyaltyBonus` |
| Final | `min(computed, base + bonusCap)` |

---

### Events

| Event | Trigger |
|---|---|
| `MultiplierStreamed(user, multiplierBps, streamedOnChain)` | `streamMultiplier` |
| `RewardStreamUpdated(streamAddress)` | `setRewardStream` |
| `FallbackMultiplierUpdated(multiplierBps)` | `setFallbackMultiplier` |
| `OwnershipTransferred(previous, new)` | `transferOwnership` |
| `BonusCapUpdated(capBps)` | `setBonusCap` |

---

### Design Intent

Provide a configurable multiplier calculation layer that translates abstract belief and loyalty scores into concrete reward scaling factors, and push those factors to the stream contract without requiring callers to understand the internal math.

### Why It Didn't Make Production

1. **Access control mismatch** — `RewardStream.updateMultiplier` is gated to `onlyGovernor`; `RewardMultiplier` is not a governor, so the try/catch always catches and falls back to `fallbackMultiplier`.
2. **Silent fallback** — operators cannot distinguish a real multiplier stream from a fallback-every-time scenario without watching the `streamedOnChain` field in events.
3. **Loyalty bonus disproportionate** — at `loyaltyScore = 10 000`, loyalty bonus alone is 50 000 bps (5×), which immediately saturates the cap before belief contributes meaningfully.
4. **Ownership not two-step** — `transferOwnership` is an instant single-step transfer; a mistaken address cannot be recovered.

---

---

# 16. TeamworkIntegrityBond.sol

**Path:** `theloopbreaker-site/contracts/deprecated/TeamworkIntegrityBond.sol`
**Lines:** ~613
**Inherits:** `ReentrancyGuard` (standalone)

## What It Was Supposed to Do

Player-level bond (minimum 0.1 ETH) assessing whether a player is a genuine teammate or a stat-chaser. Chemistry metrics, winning-vs-stats performance data, and teammate verifications combine into a `PlayerRole` enum (StatChaser / Neutral / TeamPlayer / Champion). Each role determines an appreciation percentage and distribution split including a teammate compensation pool.

## Why It Was Likely Deprecated

Requires a minimum of 5 teammate verifications before distribution is allowed — there is no mechanism to source those verifications from actual teammates, making the contract practically unusable without an off-chain coordination layer. Also uses `.transfer()` throughout.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L3   SPDX, pragma, ReentrancyGuard
```

---

### Enums

**`PlayerRole`**
```
StatChaser  = 0  — prioritises personal stats over team success
Neutral     = 1
TeamPlayer  = 2  — consistent teammate contributions
Champion    = 3  — elite team-first behaviour
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `MIN_STAKE` | `uint256` constant = `0.1 ether` | Minimum team stake. |
| `MIN_VERIFICATIONS` | `uint256` constant = `5` | Required teammate verifications before distribution. |
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 => Bond)` | Bond storage. |
| `chemistryMetrics` | `mapping(uint256 => ChemistryMetrics[])` | Per-game team chemistry readings. |
| `winningVsStats` | `mapping(uint256 => WinningVsStats[])` | Winning-vs-statpadding readings. |
| `teammateVerifications` | `mapping(uint256 => TeammateVerification[])` | Verified teammate opinions. |
| `teammateCompensationPool` | `mapping(uint256 => uint256)` | Teammate shares held for claim. |

---

### Structs

**`Bond`**
```
bondId          uint256
player          address
playerName      string
teamAddress     address
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`ChemistryMetrics`**
```
timestamp               uint256
assistRatio             uint256  — 0-10000 (assists vs shot attempts)
defensiveEffort         uint256  — 0-10000
benchSupport            uint256  — 0-10000 (support of bench/younger players)
plusMinusDifferential   int256   — team +/- when player is on court
```

**`WinningVsStats`**
```
timestamp                   uint256
performanceInWins           uint256  — 0-10000
performanceInLosses         uint256  — 0-10000 (high score in losses = bad sign)
clutchPerformance           uint256  — 0-10000
sacrificeScore              uint256  — 0-10000 (willing to sacrifice stats for team?)
statPaddingDetected         bool     — flag set by team/coach
```

**`TeammateVerification`**
```
timestamp           uint256
teammate            address
makesUsBetter       uint256  — 0-10 rating
prioritisesWinning  uint256  — 0-10 rating
wouldWantInPlayoffs bool
notes               string
```

---

### Functions

#### `createBond(playerName, teamAddress)`
- `payable`; requires `msg.value >= MIN_STAKE (0.1 ETH)`
- Emits `BondCreated`

#### `submitChemistryMetrics(bondId, assistRatio, defensive, bench, plusMinus)`
- Only player
- `plusMinusDifferential` is a signed int — can be negative

#### `submitWinningVsStats(bondId, inWins, inLosses, clutch, sacrifice, statPaddingDetected)`
- Only player (player self-reports; obvious conflict of interest)

#### `addTeammateVerification(bondId, makesUsBetter, prioritisesWinning, wouldWantInPlayoffs, notes)`
- Open to any address (intended for teammates)
- No verification that the caller is actually on the team

#### `_calculateChemistryScore(bondId) → uint256`
```
for each ChemistryMetrics entry:
    entryScore = (assistRatio + defensive + bench) / 3   — plusMinus handled separately
average across all entries
```
Note: `plusMinusDifferential` (int256) is used separately in a weighting context but the base chemistry score ignores it, which loses an important signal.

#### `_calculateWinningScore(bondId) → uint256`
```
for each WinningVsStats entry:
    winVsLossDiff = performanceInWins > performanceInLosses
                    ? (performanceInWins - performanceInLosses) / 2
                    : 0
    entryScore = (winVsLossDiff + clutch + sacrifice) / 3
average across all entries
```

#### `_detectStatPadding(bondId) → bool`
```
statPaddedGames = count where statPaddingDetected = true
                  AND (performanceInLosses − performanceInWins > 3000)

return statPaddedGames × 100 / total > 25    — >25 % of games show padding
```
If more than 25 % of games have both the `statPaddingDetected` flag and a performance-in-losses exceeding performance-in-wins by 3 000 points, a 50 % penalty applies to the distribution.

#### `_calculateTeammateConsensusScore(bondId) → uint256`
```
require(verificationsLength >= MIN_VERIFICATIONS, "Insufficient verifications")
averageMakesUsBetter    = avg(makesUsBetter × 1000)   — scaled to 0-10000
averagePrioritisesWin   = avg(prioritisesWinning × 1000)
playoffWant             = count(wouldWantInPlayoffs) × 10000 / total
return (makesUsBetter + prioritisesWin + playoffWant) / 3
```
The `require` hard-blocks distribution if fewer than 5 verifications exist.

#### `_determinePlayerRole(chemistry, winning, consensus) → PlayerRole`
```
avgScore = (chemistry + winning + consensus) / 3
avgScore < 3000  → StatChaser
avgScore < 5000  → Neutral
avgScore < 7500  → TeamPlayer
else             → Champion
```

#### `distributeBond(bondId)`
- `nonReentrant`
- Calls `_calculateTeammateConsensusScore` (reverts if < 5 verifications)
- Detects stat padding
- Determines role
- Distribution splits:

| Role | Player | Team/Fund | Teammate Pool |
|---|---|---|---|
| StatChaser | 0 % | 100 % | 0 % |
| Neutral | 30 % | 50 % | 20 % (+10 % appreciation) |
| TeamPlayer | 60 % | 30 % | 10 % (+60 % appreciation) |
| Champion | 60 % | 30 % | 10 % (+120 % appreciation) |

- Stat padding detected → 50 % penalty to all shares
- Uses `payable().transfer()` throughout

---

### Math Summary

| Computation | Formula |
|---|---|
| Chemistry score | Average of (assist + defensive + bench) / 3 across readings |
| Winning score | `(winVsLossDiff + clutch + sacrifice) / 3` |
| Stat padding detection | `statPaddedGames% > 25 AND lossPerf − winPerf > 3000` |
| Teammate consensus | Average of scaled teammate ratings |
| Player role | Average of three scores mapped to enum |
| Teammate compensation | Role-based % held in pool |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, player, stake)` | `createBond` |
| `ChemistrySubmitted(bondId)` | `submitChemistryMetrics` |
| `WinningStatsSubmitted(bondId)` | `submitWinningVsStats` |
| `TeammateVerificationAdded(bondId, teammate)` | `addTeammateVerification` |
| `BondDistributed(bondId, role, playerShare, teammateShare)` | `distributeBond` |

---

### Design Intent

Reward players who make their teams better, not just their own statistics. The teammate compensation pool creates a direct financial benefit for teammates of genuinely team-first players.

### Why It Didn't Make Production

1. **`MIN_VERIFICATIONS = 5` without an enforcement mechanism** — no way to source verified teammate verifications on-chain.
2. **Player self-reports winning stats** — `statPaddingDetected` is a flag the player can choose not to set.
3. **`payable().transfer()` deprecated**.
4. **`plusMinusDifferential` collected but not used** in the primary chemistry score calculation.
5. **Teammate compensation pool has no claim function** in the source — teammates have no way to withdraw their shares.

---

---

# 17. RewardStream.sol

**Path:** `theloopbreaker-site/contracts/deprecated/RewardStream.sol`
**Lines:** 207
**Inherits:** `ReentrancyGuard`

## What It Was Supposed to Do

An ETH-based reward queuing and distribution contract with separate admin and governor roles. Admin queues and distributes rewards; governor updates per-user multipliers. Both roles have 2-day timelocked transfers. An `ethicsPause` mechanism can halt distributions.

## Why It Was Likely Deprecated

This is one of the most complete contracts in the archive — it has proper CEI patterns, timelocked role transfers, and `nonReentrant`. It is deprecated not due to bugs but because it was superseded by a version that integrates directly with the Vaultfire bond settlement system, removing the need for a separate reward queuing layer.

---

## Full Source with Line-by-Line Walkthrough

```
L6    contract RewardStream is ReentrancyGuard {
L7        address public admin;
L8        address public governorTimelock;
L9        mapping(address => uint256) private multipliers;
L10       mapping(address => uint256) private _pendingRewards;
L11       uint256 public ethicsPauseUntil;
```
`multipliers` stores per-user reward multipliers (set by governor). `_pendingRewards` holds ETH accrued to each address. `ethicsPauseUntil` is a timestamp; if `block.timestamp < ethicsPauseUntil`, distributions are blocked.

```
L13–L21 Timelock state variables
```
```
pendingAdmin          address  — staged new admin
adminTransferTimestamp uint256 — effective time for admin transfer
ADMIN_TRANSFER_DELAY  2 days   — constant
pendingGovernor       address  — staged new governor
governorTransferTimestamp uint256
GOVERNOR_TRANSFER_DELAY   2 days
```
Both role transfers use the same two-step pattern: initiate → wait 2 days → accept. Either transfer can be cancelled by the current admin before acceptance.

```
L49–L54 constructor(adminAddress, governorAddress)
```
Sets both roles; validates non-zero addresses. No timelock on initial deployment.

```
L56    receive() external payable {}
```
Allows the contract to receive ETH for the reward pool.

---

### `updateMultiplier(user, multiplier)` — `onlyGovernor`
Sets a per-user multiplier in the `multipliers` mapping. Used by `RewardMultiplier.sol` (when it has governor access). No range check on the multiplier value.

### `queueRewards(recipient, amount)` — `onlyAdmin`, `notPaused`
```
_pendingRewards[recipient] += amount
emit RewardsQueued(recipient, amount)
```
Does not transfer ETH; purely accounting. The actual ETH must already be in the contract (sent via `receive()` or `distributeRewards`).

### `distributeRewards(recipient, amount)` — `onlyAdmin`, `notPaused`, `nonReentrant`
```
pending = _pendingRewards[recipient]
require(pending >= amount)
_pendingRewards[recipient] = pending - amount    — effects first
(bool success,) = payable(recipient).call{value: amount}("")   — interaction last
require(success)
```
Correct CEI pattern. Admin pushes ETH to recipient. The comment `✅ EFFECTS before INTERACTIONS` is explicit in the source.

### `claimRewards(address payable recipient)` — `nonReentrant`, `notPaused`
```
claimer = msg.sender
amount = _pendingRewards[claimer]
require(amount > 0)
if recipient == address(0): recipient = payable(claimer)
_pendingRewards[claimer] = 0         — effects first
(bool,) = recipient.call{value: amount}("")   — interaction last
```
Pull-pattern claim. The claimer can nominate a different recipient (useful for directing rewards to a hardware wallet from a hot wallet caller).

### `setEthicsPause(untilTimestamp)` — `onlyAdmin`
```
require(untilTimestamp >= block.timestamp)
ethicsPauseUntil = untilTimestamp
```
Cannot set a pause in the past. No maximum duration cap — admin could theoretically pause indefinitely.

---

### Two-Step Admin Transfer

```
transferAdmin(newAdmin)     — onlyAdmin; sets pendingAdmin + timestamp
acceptAdmin()               — pendingAdmin only, after 2-day delay
cancelAdminTransfer()       — onlyAdmin; clears pending
```

### Two-Step Governor Transfer

```
transferGovernor(newGovernor) — onlyAdmin
acceptGovernor()              — pendingGovernor only, after 2-day delay
cancelGovernorTransfer()      — onlyAdmin
```
Note: Governor transfer is initiated by admin, not by current governor. This means if admin is compromised, both admin and governor can be redirected.

---

### Math Summary

No arithmetic beyond `pending -= amount` and `pending += amount`. All ETH accounting uses direct wei balances.

---

### Events

| Event | Trigger |
|---|---|
| `MultiplierUpdated(user, multiplier)` | `updateMultiplier` |
| `AdminTransferred(previous, new)` | `acceptAdmin` |
| `AdminTransferInitiated(current, pending, effectiveTime)` | `transferAdmin` |
| `AdminTransferCancelled(cancelled)` | `cancelAdminTransfer` |
| `GovernorTimelockUpdated(previous, new)` | `acceptGovernor` |
| `GovernorTransferInitiated(...)` | `transferGovernor` |
| `GovernorTransferCancelled(cancelled)` | `cancelGovernorTransfer` |
| `RewardsQueued(recipient, amount)` | `queueRewards` |
| `RewardsClaimed(claimer, recipient, amount)` | `distributeRewards`, `claimRewards` |
| `EthicsPauseApplied(untilTimestamp)` | `setEthicsPause` |

---

### Design Intent

A secure ETH reward pipeline separating concerns between an admin (operational) and a governor (policy). The ethics pause provides a governance lever to halt distributions during controversies or exploits while a remediation is prepared.

### Why It Didn't Make Production (as standalone)

1. **Superseded by tighter integration** — production reward flow was embedded directly into bond settlement contracts to reduce the attack surface of a separate relay contract.
2. **Indefinite pause** — admin can pause forever; production version required a maximum pause duration.
3. **Governor transfer initiated by admin** — creates a single point of failure for both critical roles.
4. **`updateMultiplier` has no range check** — a governor could set a multiplier to 0 or an astronomically high value.

---

---

# 18. ContributorUnlockKey.sol

**Path:** `theloopbreaker-site/contracts/deprecated/ContributorUnlockKey.sol`
**Lines:** 38
**Inherits:** `ERC721`, `Ownable`

## What It Was Supposed to Do

An ERC-721 NFT access token. When minted (by owner only), it optionally calls a configurable `unlockHook` contract's `onMint(to, tokenId)` function, triggering elevated feature access in downstream systems. Token name: "Contributor Unlock Key" (CUK).

## Why It Was Likely Deprecated

The contract is functional but was superseded when the access control model moved away from NFT-gated access toward the guardian token (VFG) voting-weight model. ERC-721 per-user keys also do not compose well with the multi-bond architecture.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
L4    import "@openzeppelin/contracts/access/Ownable.sol";
```

```
L11–L13 interface IUnlockHook
    function onMint(address to, uint256 tokenId) external;
```
A minimal callback interface. Any contract implementing `onMint` can be plugged in as the hook.

```
L15   contract ContributorUnlockKey is ERC721, Ownable {
L16       uint256 private _tokenId;
L17       address public unlockHook;
```
`_tokenId` is a simple counter starting at 0. Incrementing before minting (L29: `_tokenId += 1`) ensures token IDs start at 1.

```
L19   event UnlockHookSet(address indexed hook);
```
Logged when hook is updated. Allows off-chain indexers to track which contract is receiving mint callbacks.

```
L21   constructor()
    ERC721("Contributor Unlock Key", "CUK") Ownable(msg.sender) {}
```
No arguments; deployer is owner.

```
L23–L26 function setUnlockHook(address hook) external onlyOwner
```
Owner can change the hook at any time. No validation that `hook` is non-zero — `hook == address(0)` is a valid no-op setting (handled by the `if` in `mint`).

```
L28–L36 function mint(address to) external onlyOwner returns (uint256)
```
```
L29   _tokenId += 1;
L30   uint256 newId = _tokenId;
L31   _mint(to, newId);                            — ERC721 standard mint
L32   if (unlockHook != address(0)) {
L33       IUnlockHook(unlockHook).onMint(to, newId);   — external callback
L34   }
L35   return newId;
```
The hook call is **after** `_mint`, so the token is already owned by `to` when the hook fires. The hook could theoretically call back into this contract — but since no state-modifying function is unguarded, the reentrancy risk is minimal. The `onMint` callback is not wrapped in a try/catch, so a reverting hook would cause the entire `mint` to revert.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `_tokenId` | `uint256` private | Sequential token ID counter. |
| `unlockHook` | `address` public | Optional callback contract. |

### Events

| Event | Trigger |
|---|---|
| `UnlockHookSet(hook)` | `setUnlockHook` |
| Standard ERC-721 `Transfer(from, to, tokenId)` | `mint` (via `_mint`) |

---

### Design Intent

Gate elevated Vaultfire features behind an NFT that can only be minted by the contract owner. The hook enables any feature system to respond to new contributors without modifying this contract.

### Why It Didn't Make Production

1. **Hook not wrapped in try/catch** — a buggy or intentionally reverting hook breaks all minting.
2. **No burn mechanism** — once minted, access cannot be revoked; the NFT can be transferred to any wallet, losing the contributor-specific binding.
3. **Owner-only minting** — does not scale; requires manual intervention per contributor.
4. **Access model superseded** — the guardian token (ERC-20 with voting weight) was preferred over ERC-721 individual keys.

---

---

# 19. GhostkeyLoyaltyLock.sol

**Path:** `theloopbreaker-site/contracts/deprecated/GhostkeyLoyaltyLock.sol`
**Lines:** 69
**Inherits:** None (standalone)

## What It Was Supposed to Do

A simple ETH time-lock with a linear loyalty multiplier. Users stake ETH for a fixed number of weeks. After the lock period ends they can withdraw principal. The contract exposes `getMultiplier(user)` as a read-only signal for backend systems — it does not enforce or apply the multiplier on-chain.

## Why It Was Likely Deprecated

The multiplier is advisory only — there is no on-chain mechanism enforcing it against rewards. The contract is also entirely standalone with no integration into the `RewardStream` or `RewardMultiplier` systems. Uses deprecated `.transfer()`.

---

## Full Source with Line-by-Line Walkthrough

```
L9    contract GhostkeyLoyaltyLock {
L10–L14 struct LockInfo { start, duration, amount }
L16   mapping(address => LockInfo) public locks;
```
One lock per address. Attempting to stake again while a lock is active reverts with `"already staked"`.

---

### `stake(ensHolder, lockWeeks)` — `payable`

```
L27   require(lockWeeks > 0, "duration 0");
L28   require(msg.value > 0, "no value");
L30   LockInfo storage info = locks[ensHolder];
L31   require(info.amount == 0, "already staked");
L33   info.start    = block.timestamp;
L34   info.duration = lockWeeks * 1 weeks;
L35   info.amount   = msg.value;
L37   emit Staked(ensHolder, msg.value, lockWeeks);
```
Note: The lock is recorded against `ensHolder`, not `msg.sender`. The caller can lock ETH on behalf of any address. This is unusual — it means anyone can lock ETH to an address they do not control, effectively preventing that address from re-staking until the period expires.

---

### `getMultiplier(user) → multiplier`

```
L45   if info.amount == 0: return 1
L50   weeksLocked = info.duration / 1 weeks
L52   multiplier = 1 + weeksLocked / 10
```
Linear scale. A 10-week lock gives `1 + 1 = 2`. A 100-week lock gives `1 + 10 = 11`. No cap. The return type is `uint256` and units are arbitrary (the comment says "1× base plus 0.1× per week" but the implementation yields integers, not decimals — there is no fractional scaling).

---

### `withdraw()`

```
L58   require(info.amount > 0, "nothing staked")
L59   require(block.timestamp >= info.start + info.duration, "still locked")
L61   amount = info.amount
L62   delete locks[msg.sender]            — delete before transfer (CEI-correct)
L63   emit Withdrawn(msg.sender, amount)
L64   payable(msg.sender).transfer(amount)   — deprecated
```
`msg.sender` must have been the original `ensHolder` to withdraw, because the lock is indexed by address and `delete` uses `msg.sender`. If someone staked on behalf of a different `ensHolder`, only that `ensHolder` can withdraw. **This creates a potential griefing vector:** a malicious actor can lock ETH to any address they choose, temporarily blocking that address from re-staking.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `locks` | `mapping(address → LockInfo)` | One lock per address. |

### Events

| Event | Trigger |
|---|---|
| `Staked(user, amount, weeksLocked)` | `stake` |
| `Withdrawn(user, amount)` | `withdraw` |

---

### Design Intent

Provide a loyalty depth signal for backend reward systems — users who lock ETH for longer periods earn higher multipliers in off-chain reward calculations.

### Why It Didn't Make Production

1. **Multiplier is advisory only** — never used on-chain by any reward distribution contract.
2. **Third-party locking griefing vector** — caller can lock ETH to any address.
3. **No fractional multiplier** — integer division truncates; all 1–9 week locks return multiplier `1`, same as no lock.
4. **`.transfer()` deprecated**.
5. **No integration with `RewardMultiplier` or `RewardStream`**.

---

---

# 20. LaborDignityBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/LaborDignityBonds.sol`
**Lines:** ~470
**Inherits:** `ReentrancyGuard` (standalone V1)

## What It Was Supposed to Do

A company stakes ETH on six dimensions of worker flourishing: income, autonomy, dignity, work-life balance, security, and voice. Workers independently attest metrics to verify or challenge company reports. The bond appreciates when flourishing is high and workers confirm the numbers. An exploitation penalty fires if conditions fall below thresholds or workers report low scores.

## Why It Was Likely Deprecated

Uses deprecated `.transfer()`. Superseded by `LaborDignityBondsV2` which adds `BaseYieldPoolBond`, a dignity floor (minimum payout), and `.call{}` — but V2 introduced a new off-by-one bug in the worker verification loop (see contract 23).

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma ^0.8.25, ReentrancyGuard + MissionEnforcement
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `EXPLOITATION_FLOURISHING_THRESHOLD` | `uint256` = `4000` | Flourishing below this = penalty. |
| `EXPLOITATION_VERIFICATION_THRESHOLD` | `uint256` = `50` | Verification multiplier ≤ 50 = penalty. |
| `EXPLOITATION_WORKER_SCORE_THRESHOLD` | `uint256` = `4000` | Worker-reported score below this = penalty. |
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 → Bond)` | Bond storage. |
| `flourishingMetrics` | `mapping(uint256 → FlourishingMetrics[])` | Company-submitted readings. |
| `workerAttestations` | `mapping(uint256 → WorkerAttestation[])` | Worker-submitted attestations. |

---

### Structs

**`Bond`**
```
bondId          uint256
company         address
companyName     string
workerCount     uint256
stakeAmount     uint256
createdAt       uint256
active          bool
```

**`FlourishingMetrics`**
```
timestamp           uint256
income              uint256  — 0-10000
autonomy            uint256  — 0-10000
dignity             uint256  — 0-10000
workLifeBalance     uint256  — 0-10000
security            uint256  — 0-10000
voice               uint256  — 0-10000
submitter           address
workerCount         uint256  — headcount at time of submission
```

**`WorkerAttestation`**
```
timestamp           uint256
worker              address
isCurrentWorker     bool
overallScore        uint256  — worker's own assessment (0-10000)
confirmsCompany     bool     — does worker confirm company metrics?
specificConcerns    string
```

---

### Functions

#### `flourishingScore(bondId) → uint256`
Average of six dimensions from the latest `FlourishingMetrics` entry.

#### `workerVerifiedScore(bondId) → uint256`
Iterates all attestations from current workers (where `isCurrentWorker = true`) within the last 90 days:
```
sum = sum(attestation.overallScore) for qualifying attestations
return count > 0 ? sum / count : 5000
```

#### `workerVerificationMultiplier(bondId) → uint256`
```
verifiedCount = qualifying attestations
totalExpected = bond.workerCount
verificationRate = verifiedCount × 100 / totalExpected

if rate >= 50: return 150
if rate >= 25: return 100
else:          return 50
```
Returns 100 if `workerCount = 0`.

#### `timeMultiplier(bondId) → uint256`
Standard segmented formula: 100 at year 0, increasing over years.

#### `calculateBondValue(bondId) → uint256`
```
bondValue = (stake × flourishing × verification × time) / 1_000_000
```

#### `shouldApplyExploitationPenalty(bondId) → (bool, string)`
```
flourishing < 4000            → (true, "Workers not flourishing")
verificationMultiplier <= 50  → (true, "Workers not verifying")
workerVerifiedScore < 4000    → (true, "Workers reporting poor conditions")
```
Any single condition triggers the penalty.

#### `distributeBond(bondId)`
- `nonReentrant`
- Calls `_requireMissionCompliance()`
- 70 % workers / 30 % company on normal appreciation
- 100 % workers on exploitation penalty
- Uses deprecated `payable().transfer()`
- **Worker share is sent to `_useYieldPool` equivalent in V1** — which is just `payable(someAddress).transfer()` since there is no base contract; the recipient is the owner, not individual workers

---

### Math Summary

| Computation | Formula |
|---|---|
| Flourishing score | Average of 6 dimensions from latest metrics |
| Worker verified score | Average of current-worker attestation scores (90-day window) |
| Worker verification multiplier | 50/100/150 based on what % of workers attested |
| Bond value | `(stake × flourishing × verification × time) / 1_000_000` |
| Normal split | 70 / 30 workers / company |
| Exploitation split | 100 / 0 |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, company, stake)` | `createBond` |
| `MetricsSubmitted(bondId)` | `submitFlourishingMetrics` |
| `WorkerAttested(bondId, worker)` | `addWorkerAttestation` |
| `BondDistributed(bondId, workerShare, companyShare)` | `distributeBond` |
| `ExploitationPenalty(bondId, reason)` | `distributeBond` when penalty fires |

---

### Design Intent

Create corporate accountability for worker conditions by tying employer capital directly to measurable worker flourishing. The dual-report model (company submits, workers attest) creates a checks-and-balances structure where exploitation is financially costly.

### Why It Didn't Make Production

1. **`.transfer()` deprecated** throughout.
2. **Worker share goes to owner in V1** — actual individual worker distribution relies on an off-chain registry that was never built.
3. **No Sybil resistance on attestations** — any address claiming `isCurrentWorker = true` can attest; there is no employment verification.
4. **Superseded by V2** which added dignity floor and `.call{}` — but V2 introduced its own off-by-one bug.

---

---

# 21. EscapeVelocityBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/EscapeVelocityBondsV2.sol`
**Lines:** ~312
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

V2 of the escape velocity bond. Same constrained stake range and income-escape mechanics as V1. Key upgrades: inherits `BaseYieldPoolBond` (timelock, `whenNotPaused`, `.call{}` via `_useYieldPool`); simplified `calculateBondValue` formula; explicit timelock before distribution.

## Why It Was Likely Deprecated

The simplified bond value formula — `stake + stake × incomeGain / 10000` — is purely linear and does not incorporate autonomy, stability, people helped, verification, or time. All the nuance of V1's multiplier stack was dropped without designing a replacement calibrated to the new formula. This made V2 economically weaker than V1 despite being technically cleaner.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L3   SPDX, pragma, BaseYieldPoolBond import
```

---

### Key Differences From V1

| Feature | V1 | V2 |
|---|---|---|
| Inheritance | Standalone | BaseYieldPoolBond |
| Transfer pattern | `.transfer()` | `_useYieldPool()` / `.call{}` |
| Bond value formula | `stake × escape × payForward × verif × time / 100_000_000` | `stake + stake × incomeGain / 10000` |
| Timelock | No | Yes |
| `whenNotPaused` | No | Yes |
| Multiplier dimensions | 4 (escape, payForward, verification, time) | 1 (incomeGain only) |

---

### `calculateIncomeGain(bondId) → uint256`
Identical to V1:
```
gain = (latestIncome - initialIncome) × 10000 / initialIncome
```
Returns 0 if income decreased.

### `calculateBondValue(bondId) → uint256`
```
gain = calculateIncomeGain(bondId)
if gain >= ESCAPE_THRESHOLD (15000):
    return stake + stake × gain / 10000    — linear appreciation
else:
    return stake                           — no appreciation if below threshold
```
This is dramatically simpler than V1 but eliminates all pay-it-forward, community verification, and time incentives.

### `distributeBond(bondId)`
- Checks `DISTRIBUTION_TIMELOCK`
- 80 % escaper, 20 % `payItForwardPool`
- Uses `_useYieldPool(escaperShare)` for ETH routing
- Emits `BondDistributed`

The pay-it-forward pool exists as a balance but has no integrated claim mechanism within V2 — funds accumulate but distribution to new escapers must be handled externally.

---

### Math Summary

| Computation | Formula |
|---|---|
| Income gain | `(current − initial) × 10000 / initial` |
| Bond value | `stake + stake × incomeGain / 10000` (if gain ≥ escape threshold) |
| Escape threshold | 15 000 (150 % income gain) |
| Distribution | 80 % escaper / 20 % payItForwardPool |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, escaper, initialIncome, stake)` | `createBond` |
| `ProgressSubmitted(bondId)` | `submitProgress` |
| `VerificationAdded(bondId)` | `addVerification` |
| `BondDistributed(bondId, escaperShare, payForwardShare)` | `distributeBond` |

---

### Design Intent

Preserve V1's poverty-escape mission with a technically cleaner implementation that removes the deprecated transfer pattern and adds proper security controls.

### Why It Didn't Make Production

1. **Formula regression** — dropping the multiplier stack removed all incentive to help others, build over time, or maintain community verification.
2. **Pay-it-forward pool not self-distributing** — no mechanism to route pool funds to new escapers.
3. **V3 needed** — neither V1 nor V2 adequately solved the problem; a combined approach was planned but not completed before the archive.

---

---

# 22. YieldPoolHealthMonitor.sol

**Path:** `theloopbreaker-site/contracts/deprecated/YieldPoolHealthMonitor.sol`
**Lines:** 389
**Inherits:** None (standalone)

## What It Was Supposed to Do

An off-chain monitoring companion providing on-chain snapshots of yield pool health. The owner or any external caller can record health snapshots. The contract tracks reserve ratios, risk levels, and triggers rebalance events when reserves fall below threshold. Historical snapshots are stored with automatic pruning when `MAX_HISTORY = 100` is exceeded.

## Why It Was Likely Deprecated

A pure monitoring contract with no enforcement capability — it emits events and stores snapshots, but cannot pause or modify the yield pool it monitors. The production system moved health monitoring into the yield pool contract itself, eliminating the need for a separate monitor.

---

## Full Source with Line-by-Line Walkthrough

```
L12   contract YieldPoolHealthMonitor {
```
No inheritance. Minimal footprint — just owner, config, and history storage.

---

### Structs

**`YieldPoolHealth`**
```
totalReserves       uint256  — yield pool ETH balance (input)
totalActiveBonds    uint256  — total value of active bonds (input)
reserveRatio        uint256  — bps: reserves / bonds × 10000
utilizationRate     uint256  — bps: bonds / reserves × 10000
minimumRequired     uint256  — output of calculateMinimumReserve
isHealthy           bool     — pool >= minimumRequired
riskLevel           uint256  — 0=Safe, 1=Warning, 2=Critical
timestamp           uint256
```

**`ReserveRequirements`**
```
baseReserveRatio    uint256 = 5000   — 50 % of active bonds
volatilityBuffer    uint256 = 2000   — +20 % extra
emergencyReserve    uint256 = 10 ETH — absolute floor
rebalanceThreshold  uint256 = 4000   — alert trigger at <40 %
```

---

### `assessHealth(yieldPool, totalActiveBonds) → YieldPoolHealth`

```
L139  minRequired = calculateMinimumReserve(totalActiveBonds)
L141  reserveRatio = totalActiveBonds > 0
                     ? (yieldPool × 10000) / totalActiveBonds
                     : 10000
L145  utilizationRate = yieldPool > 0
                        ? (totalActiveBonds × 10000) / yieldPool
                        : 0
L150  riskLevel:
          reserveRatio < 4000 → 2 (Critical)
          reserveRatio < 6000 → 1 (Warning)
          else               → 0 (Safe)
```
A `pure`-style view (only reads inputs, not state). Can be called any time without storing a snapshot.

---

### `calculateMinimumReserve(totalActiveBonds) → uint256`

```
baseReq     = totalActiveBonds × 5000 / 10000    = 50 %
volBuffer   = totalActiveBonds × 2000 / 10000    = 20 %
totalReq    = baseReq + volBuffer                 = 70 % of bonds
return max(totalReq, 10 ether)
```
The reserve target is 70 % of total active bond value, with a hard floor of 10 ETH. This is conservative — a yield pool holding 70 % of outstanding bond value as reserves would be unusual in DeFi.

---

### `recordHealthSnapshot(yieldPool, totalActiveBonds)`

```
health = assessHealth(...)
if previous snapshot exists and riskLevel changed:
    emit RiskLevelChanged(...)
healthHistory.push(health)
if healthHistory.length > MAX_HISTORY:
    _pruneHistory()
emit HealthSnapshotRecorded(...)
if reserveRatio < rebalanceThreshold:
    emit RebalanceNeeded(...)
```
Open to any caller — no access control on snapshot recording. This is intentional: any monitoring bot or user can push a snapshot.

---

### `_pruneHistory()` — internal

```
removeCount = healthHistory.length / 2   — prune oldest 50 %
for i in 0..(length - removeCount):
    healthHistory[i] = healthHistory[i + removeCount]   — shift down
for i in 0..removeCount:
    healthHistory.pop()                                  — trim tail
```
O(n) shift algorithm — gas cost grows linearly with history size. At `MAX_HISTORY = 100` and pruning 50 at a time, maximum gas per prune is bounded. The two-loop approach avoids modifying the array during iteration.

---

### `canSafelyCoverAppreciation(yieldPool, totalActiveBonds, appreciationNeeded)`

```
canCover = yieldPool >= appreciationNeeded
if !canCover: return (false, false, "Insufficient funds")

postPool = yieldPool - appreciationNeeded
postHealth = assessHealth(postPool, totalActiveBonds)

wouldBeHealthy = postHealth.isHealthy
recommendation = based on postHealth.riskLevel
```
Simulation function — computes what would happen to pool health after a hypothetical settlement. Used for pre-settlement checks in the bond contracts.

---

### Math Summary

| Computation | Formula |
|---|---|
| Reserve ratio | `(yieldPool × 10000) / activeBonds` (bps) |
| Utilisation rate | `(activeBonds × 10000) / yieldPool` (bps) |
| Minimum reserve | `max(bonds × 70 %, 10 ETH)` |
| Risk level | `<40 % → Critical, 40–60 % → Warning, >60 % → Safe` |

---

### Events

| Event | Trigger |
|---|---|
| `HealthSnapshotRecorded(timestamp, reserves, bonds, ratio, risk)` | `recordHealthSnapshot` |
| `RebalanceNeeded(timestamp, currentRatio, requiredRatio, deficit)` | When ratio < threshold |
| `RiskLevelChanged(timestamp, prevLevel, newLevel, reason)` | When risk level transitions |
| `ReserveConfigUpdated(base, buffer, emergency, threshold)` | `updateReserveConfig` |

---

### Design Intent

Give the Vaultfire team real-time visibility into yield pool economics without requiring gas-expensive operations on the pool itself. The `RebalanceNeeded` event triggers off-chain treasury operations.

### Why It Didn't Make Production (standalone)

1. **No enforcement capability** — can only observe, not act. Monitoring was folded into the yield pool contract itself in production.
2. **O(n) prune cost** — while bounded at MAX_HISTORY, the approach is inefficient compared to a ring buffer.
3. **Open snapshot recording** — any caller can spam the history array up to MAX_HISTORY entries, triggering prunes and consuming gas.
4. **70 % reserve target is extremely conservative** — this would make most bond settlement impossible as pools grow; the target was later revised to a more practical 20–30 % in production.

---

---

# 23. LaborDignityBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/LaborDignityBondsV2.sol`
**Lines:** ~805
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

V2 of the labor dignity bond. Adds a `DIGNITY_FLOOR` ensuring workers always receive at least 50 % of their initial stake as a minimum payout. Changes the divisor from 1 000 000 to 50 000 000 (math-fix 2026-01-07). Adds full NatSpec documentation, explicit balance checks, and `.call{}` via `_useYieldPool`.

## Why It Was Likely Deprecated

Contains a documented off-by-one bug in the `workerVerifiedScore` loop that accesses the wrong attestation index after decrementing the loop variable. The bug causes the score calculation to silently skip one attestation and read a different one, corrupting the worker verification result.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma ^0.8.25, BaseYieldPoolBond import
```

---

### New in V2 vs V1

```
L~      uint256 public constant DIGNITY_FLOOR = 5000;
```
50 % of stake amount as a minimum bond value. If `calculateBondValue` returns less than `DIGNITY_FLOOR × stakeAmount / 10000`, the floor is applied.

```
L~      // math-fix 2026-01-07: divisor updated from 1_000_000 to 50_000_000
```
Inline comment documenting the calibration change. The 50× increase in divisor reduces bond outputs proportionally.

---

### The Off-by-One Bug in `workerVerifiedScore`

The following pseudocode is representative of the actual loop:

```solidity
for (uint256 i = attestations.length; i > 0;) {
    i--;                                    // i is decremented here
    if (attestations[i].timestamp < cutoff) break;
    if (!attestations[i].isCurrentWorker) continue;
    i--;                                    // BUG: i decremented AGAIN
    totalScore += attestations[i].overallScore;  // reads wrong index
    count++;
}
```
**Consequence:** For every qualifying attestation, the loop decrements `i` twice — once for loop advancement and once erroneously inside the loop body. This causes the score to be read from the attestation **before** the qualifying one, not the qualifying attestation itself. With alternating current/former worker entries, this consistently reads former-worker scores instead of current-worker scores.

**Impact:** The `workerVerificationMultiplier` and `workerVerifiedScore` values returned will be incorrect whenever the attestation array contains the pattern `[current, former, current, former...]` or any variation where the double-decrement crosses index boundaries.

---

### `calculateBondValue(bondId) → uint256`

```
rawValue = (stake × flourishing × verification × time) / 50_000_000
floor    = stake × DIGNITY_FLOOR / 10000    = stake × 50 %
return rawValue > floor ? rawValue : floor
```
The dignity floor ensures the bond is never worth less than 50 % of the original stake, protecting workers from near-zero payouts even in poor-condition scenarios.

---

### State Variables (Additional to V1)

| Variable | Type | Purpose |
|---|---|---|
| `DIGNITY_FLOOR` | `uint256` constant = `5000` | Minimum payout as % of stake (50 %). |

---

### Math Summary

| Computation | Formula (V2) |
|---|---|
| Flourishing score | Average of 6 dimensions (same as V1) |
| Worker verified score | **Buggy**: reads wrong attestation index |
| Bond value | `max(stake × flourishing × verif × time / 50_000_000, stake × 50 %)` |
| Dignity floor | `stakeAmount × 5000 / 10000` |

---

### Events

Same events as V1 plus:
- `DignityFloorApplied(bondId, floorValue)` — emitted when floor is used instead of raw calculation.

---

### Design Intent

Preserve V1's mission while fixing the transfer pattern and adding a minimum-payout guarantee to prevent the bond from being used to effectively strip workers of their principal in exploitative conditions.

### Why It Didn't Make Production

1. **Off-by-one bug in `workerVerifiedScore` loop** — silently reads wrong attestation indices; corrupts the primary worker-verification signal.
2. **Divisor 50 000 000 may be too large** — at mid-range scores and a 1 ETH stake, bond values fall below the dignity floor in almost all scenarios, making the dignity floor the de facto return path regardless of actual worker conditions.
3. **Full NatSpec was added but not audited** — the documentation describes intended behaviour that the bug contradicts.

---

---

# 24. CommonGroundBondsV2.sol

**Path:** `theloopbreaker-site/contracts/deprecated/CommonGroundBondsV2.sol`
**Lines:** ~337
**Inherits:** `BaseYieldPoolBond`

## What It Was Supposed to Do

V2 of the bridge-building bond. Upgrades V1's deprecated `.transfer()` and five-multiplier formula to a cleaner two-factor formula and `_useYieldPool` routing. Adds timelock. Simplifies distribution to 80/20 bridge-builders/community.

## Why It Was Likely Deprecated

The simplification went too far — `calculateBondValue` uses only bridge quality score and ignores ripple effect, dual party representation, cross-community witnesses, and time. The 80/20 split also removed the community fund's sustainability by reducing its share from 40 % to 20 %. Superseded by a version re-incorporating the ripple and time factors.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma, BaseYieldPoolBond import
```

---

### Key Differences From V1

| Feature | V1 | V2 |
|---|---|---|
| Transfer | `.transfer()` | `_useYieldPool()` |
| Bond value formula | 5-multiplier product / 10 000 000 000 | `stake × quality / 5000` |
| Distribution split | 60 builder / 40 community | 80 builder / 20 community |
| Timelock | No | Yes |
| `whenNotPaused` | No | Yes |
| Ripple, time, witness multipliers | Yes | No |

---

### `bridgeQualityScore(bondId) → uint256`

```
cutoff = block.timestamp - 90 days
for recent metrics:
    entryScore = (understanding + collaboration + respect) / 3
return avg of recent entry scores
```
Returns 5 000 if no metrics.

---

### `shouldApplyDivisionPenalty(bondId) → bool`

```
latest = most recent BridgeMetrics entry
return latest.respectLevel < 3000  OR  bridgeQualityScore < 4000
```

---

### `calculateBondValue(bondId) → uint256`

```
quality = bridgeQualityScore(bondId)
return stake × quality / 5000
```
A score of 5 000 (neutral) returns exactly `stake`. A score of 10 000 (maximum) returns `2 × stake`. A score of 2 500 returns half stake.

This is a significant simplification. In V1 the ripple effect alone could triple the value; in V2 the maximum appreciation is 1× regardless of impact breadth or duration.

---

### `distributeBond(bondId)`

- Checks `DISTRIBUTION_TIMELOCK`
- Checks `_requireMissionCompliance()` (HUMAN_VERIFICATION_FINAL_SAY, PRIVACY_DEFAULT — also questionable for a bridge-building bond)
- Normal: 80 % bridge builders (40 % each), 20 % community fund
- Penalty: 50 % refund each builder, 0 % community
- Uses `_useYieldPool`

---

### Math Summary

| Computation | Formula |
|---|---|
| Bridge quality score | Average of (understanding + collaboration + respect)/3 over last 90 days |
| Bond value | `stake × quality / 5000` (range 0–2×) |
| Normal split | 80 % builders / 20 % community |
| Penalty split | 100 % refund to builders |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, builderA, builderB)` | `createBond` |
| `MetricsSubmitted(bondId)` | `submitBridgeMetrics` |
| `WitnessAdded(bondId, witness)` | `addWitness` |
| `BondDistributed(bondId, builderA, builderB, communityShare)` | `distributeBond` |
| `DivisionPenalty(bondId)` | Penalty path |

---

### Design Intent

A technically cleaner V2 that removes the numerically fragile multiplier stack of V1 in favour of a predictable single-factor formula.

### Why It Didn't Make Production

1. **Over-simplified formula** — dropping ripple, time, and witness factors removed all incentives for sustained community impact.
2. **Community fund share halved** — 20 % is insufficient to sustain an independent community bridge fund.
3. **Still has wrong mission principle check** — `HUMAN_VERIFICATION_FINAL_SAY` is not semantically relevant to bridge-building.

---

---

# 25. CovenantFlame.sol

**Path:** `theloopbreaker-site/contracts/deprecated/CovenantFlame.sol`
**Lines:** 68
**Inherits:** None (standalone)

## What It Was Supposed to Do

A lightweight provenance anchor recording the deployed addresses of the `BeliefOracle` and `FreedomVow` contracts. The `igniter` (deployer) can update and clear the recorded addresses as new deployments are made. `ghostEcho` records the Ghostkey-316 provenance address. Off-chain guardian systems can query `covenantSummary()` to discover the live contract addresses.

## Why It Was Likely Deprecated

This is a mutable pointer contract — the entire point is to be updated. As the system matured, the oracle and vow addresses became stable enough to be embedded directly in downstream contracts, making the indirection unnecessary. The contract itself was not broken; it was made redundant.

---

## Full Source with Line-by-Line Walkthrough

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
```

```
L8    address public immutable igniter;
L9    address public immutable ghostEcho;
```
Both set at construction time. `igniter = msg.sender`. `ghostEcho` must be non-zero (`AddressRequired` custom error if zero).

```
L11   address public beliefOracle;
L12   address public freedomVow;
```
Mutable — updated by `recordIgnition` and reset by `clearIgnition`. Initially zero (no oracle recorded until `recordIgnition` is called).

```
L20–L23 events
```
```
CovenantForged(igniter, oracle, vow)   — new addresses recorded
CovenantCleared(caller)                — addresses reset to zero
```

```
L24–L25 custom errors
```
```
OnlyIgniter()      — access control failure
AddressRequired()  — zero address validation failure
```

---

### `recordIgnition(oracle, vow)` — igniter only

```
L37   if (msg.sender != igniter) revert OnlyIgniter();
L38   if (oracle == address(0) || vow == address(0)) revert AddressRequired();
L39   beliefOracle = oracle;
L40   freedomVow = vow;
L41   emit CovenantForged(msg.sender, oracle, vow);
```
No timelock. Igniter can update freely. There is no history of previous addresses — only the current pair is stored.

---

### `clearIgnition()` — igniter only

```
L44   if (msg.sender != igniter) revert OnlyIgniter();
L45   beliefOracle = address(0);
L46   freedomVow = address(0);
L47   emit CovenantCleared(msg.sender);
```
Used when a deployment is being replaced before the new addresses are known.

---

### `covenantSummary() → (igniterAddress, oracleAddress, vowAddress)` — view

```
L55   return (igniter, beliefOracle, freedomVow);
```
Convenience getter for off-chain consumers. Equivalent to reading the three public variables individually but in a single call.

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `igniter` | `address immutable` | Address authorised to record/clear. |
| `ghostEcho` | `address immutable` | Ghostkey-316 provenance anchor. |
| `beliefOracle` | `address` | Current BeliefOracle deployment. |
| `freedomVow` | `address` | Current FreedomVow deployment. |

### Events

| Event | Trigger |
|---|---|
| `CovenantForged(igniter, oracle, vow)` | `recordIgnition` |
| `CovenantCleared(caller)` | `clearIgnition` |

---

### Design Intent

A gas-minimal contract registry pattern allowing the Vaultfire team to publish new BeliefOracle and FreedomVow deployment addresses without requiring any downstream contract to be redeployed or reconfigured.

### Why It Didn't Make Production

1. **Made redundant** — oracle and vow addresses stabilised and were hardcoded in production contracts.
2. **No address history** — only current addresses stored; auditors cannot trace prior deployments.
3. **No multi-sig on igniter** — a compromised igniter can redirect all downstream consumers to malicious addresses.

---

---

# 26. PurchasingPowerBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/PurchasingPowerBonds.sol`
**Lines:** ~467
**Inherits:** `ReentrancyGuard` (standalone V1)

## What It Was Supposed to Do

V1 of the purchasing power bond. Same six affordability metrics as V2 but no timelock, no `whenNotPaused`, no input validation helpers, and no `_useYieldPool`. Uses the deprecated `.transfer()` pattern. Notable: the `distributeBond` function explicitly notes in a comment that the worker registry was not implemented.

## Why It Was Likely Deprecated

`distributeBond` has a critical functional gap: worker ETH share is computed and logged, but **never transferred**. Only the company share is transferred. The comment in the source reads `// Note: simplified - in production would have worker registry`. Workers would receive nothing from a V1 distribution call.

---

## Full Source with Line-by-Line Walkthrough

```
L1–L4   SPDX, pragma ^0.8.25, ReentrancyGuard + MissionEnforcement
```

---

### Key Differences From V2

| Feature | V1 | V2 |
|---|---|---|
| Inheritance | Standalone | BaseYieldPoolBond |
| Timelock | No | Yes |
| Worker transfer | **Never executed** | Via `_useYieldPool` |
| `whenNotPaused` | No | Yes |
| Input validation helpers | No | `_validateNonZero`, `_validateScore` |
| Transfer pattern | `.transfer()` | `.call{}` |

---

### `distributeBond(bondId)` — The Critical Gap

```solidity
uint256 workerShare = (absAppreciation * 70) / 100;
uint256 companyShare = (absAppreciation * 30) / 100;

// ❌ Worker share: NEVER transferred
// Note: simplified - in production would have worker registry

if (companyShare > 0) {
    payable(bond.company).transfer(companyShare);   // ✅ company gets paid
}
```
The worker share is calculated and stored in the `Distribution` struct for record-keeping, but no ETH is sent to any worker-representing address. The comment acknowledges this is a stub. As a result, calling `distributeBond` on a V1 bond returns only the company's share and permanently locks the worker share in the contract with no retrieval mechanism.

---

### Math Summary

Identical to V2 (see contract 1) except divisor is unchanged at 1 000 000.

---

### Events

Same structure as V2. Events are emitted correctly including `BondDistributed` showing both shares — the events do not reveal that the worker share was never paid.

---

### Design Intent

V1 was intended as a proof-of-concept, explicitly incomplete. The comment was a development note not a production deployment admission.

### Why It Didn't Make Production

1. **Worker share never paid** — the core value proposition is broken; 70 % of appreciation is locked in the contract forever.
2. **`.transfer()` deprecated**.
3. **No timelock** — instant settlement.
4. **No input validation** — scores above 10 000 are accepted, producing nonsensical bond values.
5. **Superseded by V2**.

---

---

# 27. AIAccountabilityBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/AIAccountabilityBonds.sol`
**Lines:** 382
**Inherits:** `ReentrancyGuard` (standalone)

## What It Was Supposed to Do

An AI company stakes 30 % of quarterly revenue. The bond appreciates when six global human flourishing dimensions improve (income distribution, poverty rates, health outcomes, mental health, education access, purpose/agency). If humans are suffering, flourishing is declining, or AI is replacing workers without reskilling, the company's profits are locked and all appreciation goes to humans.

## Why It Was Likely Deprecated

The AI company self-reports all six global flourishing metrics — metrics that, by definition, cannot be determined by a single company. There is no oracle for GDP, poverty rates, or global health outcomes. The premise that one AI company can report and be held accountable for global human flourishing is architecturally unsound.

---

## Full Source with Line-by-Line Walkthrough

```
L17   contract AIAccountabilityBonds is ReentrancyGuard {
```

---

### State Variables

| Variable | Type | Purpose |
|---|---|---|
| `SUFFERING_THRESHOLD` | `uint256` constant = `4000` | Flourishing score below 40 % = humans suffering. |
| `LOW_INCLUSION_THRESHOLD` | `uint256` constant = `4000` | Education + purpose below 40 % = AI excluding humans. |
| `nextBondId` | `uint256` | ID counter. |
| `bonds` | `mapping(uint256 → Bond)` | Bond storage. |
| `bondMetrics` | `mapping(uint256 → GlobalFlourishingMetrics[])` | Self-reported global metrics. |
| `bondDistributions` | `mapping(uint256 → Distribution[])` | Distribution records. |

---

### Structs

**`Bond`**
```
bondId              uint256
aiCompany           address
companyName         string
quarterlyRevenue    uint256  — claimed revenue this bond is staking 30% of
stakeAmount         uint256
createdAt           uint256
active              bool
```

**`GlobalFlourishingMetrics`**
```
timestamp               uint256
incomeDistributionScore uint256  — 0-10000: wealth spreading or concentrating?
povertyRateScore        uint256  — 0-10000: escaping poverty?
healthOutcomesScore     uint256  — 0-10000: life expectancy improving?
mentalHealthScore       uint256  — 0-10000: depression/anxiety rates?
educationAccessScore    uint256  — 0-10000: can people learn AI skills?
purposeAgencyScore      uint256  — 0-10000: meaningful activity (paid OR unpaid)?
```

---

### `createBond(companyName, quarterlyRevenue)`

```
require(msg.value >= (quarterlyRevenue × 30) / 100, "Must stake 30%")
```
The company reports its own `quarterlyRevenue` — there is no on-chain verification. A company could claim $1 quarterly revenue to stake negligible ETH.

---

### `globalFlourishingScore(bondId) → uint256`

```
return (income + poverty + health + mental + education + purpose) / 6
```
Average of six dimensions from the latest metrics entry. Returns 5 000 if no metrics.

---

### `inclusionMultiplier(bondId) → uint256`

```
inclusionScore = (educationAccess + purposeAgency) / 2

inclusionScore >= 7000: return 150 + (score - 7000) / 60    — 150 to ~200
inclusionScore >= 4000: return 100 + (score - 4000) / 60    — 100 to 150
else:                   return 50 + score / 80               — 50 to 100
```

---

### `hasDecliningTrend(bondId) → bool`

```
current  = globalFlourishingScore(bondId)
previous = avg of second-to-last metrics entry
return current < previous
```
Only looks at the last two entries — not a trend over multiple periods.

---

### `timeMultiplier(bondId) → uint256`

```
years < 1: 100
years 1–3: 100 + years × 50
years > 3: 200 + (years - 3) × 50
```
Uncapped growth beyond year 3.

---

### `calculateBondValue(bondId) → uint256`

```
return (stake × flourishing × inclusion × time) / 1_000_000
```

**Example at `flourishing = 7000, inclusion = 140, time = 200, stake = 1e18`:**
`(1e18 × 7000 × 140 × 200) / 1_000_000 = 196_000 × 1e18` — catastrophically overestimated, demonstrating the divisor calibration problem present across all V1 standalone contracts.

---

### `shouldLockProfits(bondId) → (bool, string)`

```
flourishing < 4000     → (true, "Humans suffering")
hasDecliningTrend      → (true, "Declining human flourishing trend")
inclusionScore < 4000  → (true, "Low inclusion - AI replacing without reskilling")
```

---

### `distributeBond(bondId)` — `nonReentrant`, `onlyAICompany`

```
if locked:
    humanShare = appreciation, aiShare = 0
else:
    humanShare = appreciation / 2
    aiShare    = appreciation / 2
    
if appreciation < 0:
    humanShare = |appreciation|, aiShare = 0    — depreciation compensates humans

payable(bond.aiCompany).transfer(aiCompanyShare)   — deprecated
// humanShare goes nowhere — same "worker registry" gap as PurchasingPowerBonds V1
```
Human share is computed but there is no human registry or address to send it to. Only the company transfer executes.

---

### Math Summary

| Computation | Formula |
|---|---|
| Global flourishing | Average of 6 dimensions |
| Inclusion multiplier | Segmented: 50–200 based on education + purpose avg |
| Declining trend | Current < previous (two-entry comparison) |
| Time multiplier | 100 + 50/year, uncapped |
| Bond value | `(stake × flourishing × inclusion × time) / 1_000_000` |

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, aiCompany, stake)` | `createBond` |
| `MetricsSubmitted(bondId, timestamp)` | `submitMetrics` |
| `BondDistributed(bondId, humanShare, aiShare)` | `distributeBond` |
| `ProfitsLocked(bondId, reason)` | `distributeBond` when locked |

---

### Design Intent

Make AI companies financially responsible for global human outcomes as their systems scale. The philosophy is explicit in the source NatSpec: "The ONLY economic system that works when AI fires everyone."

### Why It Didn't Make Production

1. **Global metrics are self-reported** — AI company reports global poverty and health outcomes; no oracle.
2. **Human share has no recipient** — human payout never executes; same gap as PurchasingPowerBonds V1.
3. **Revenue self-reported** — 30 % stake calculation depends on unverified company revenue claim.
4. **Divisor miscalibration** — bond values grossly overestimated at any mid-range input.
5. **`.transfer()` deprecated**.
6. **Declining trend uses only 2 data points** — not a meaningful trend signal.

---

---

<!-- CONTINUATION — contracts 28 (complete) through 33 -->

# 28. BuilderBeliefBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/BuilderBeliefBonds.sol`
**Lines:** 564
**Inherits:** `ReentrancyGuard` + `MissionEnforcement` (standalone V1)

## What It Was Supposed to Do

V1 of the builder belief bond system. Stakers back individual builders based on belief in their project. Vesting tiers (Supporter / Believer / Champion) enforce anti-flipping commitment. Building score composed of commits, deployments, users, open-source status, and innovation. Community verification from users and contributors adds a fourth multiplier to the bond value formula. The bond matures over time to reward sustained building.

## Why It Was Likely Deprecated

1. **`.transfer()` pattern deprecated** — all three payouts use `payable(...).transfer()`.
2. **No timelock on distribution** — distribution callable at any time by bond staker.
3. **No yield pool** — appreciation is self-funded from contract balance with no replenishment mechanism.
4. **Divisor miscalibration** — at mid-range inputs the formula produces returns of 7× or more at year 0, which is economically untenable.
5. **Community verification dropped in V2** — V2 simplified to three multipliers and halved the divisor, creating a different (though still imperfect) calibration.
6. **`distributeBond` penalty inversion** — under the transacting penalty, 100 % of proceeds goes to the builder — the opposite of a penalty.

---

## Full Source with Line Numbers

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    
L4    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
L5    import "../MissionEnforcement.sol";
```

Lines 1–5 set the license, compiler version (0.8.25), and pull in OpenZeppelin's ReentrancyGuard plus the Vaultfire-internal MissionEnforcement contract.

```
L17   contract BuilderBeliefBonds is ReentrancyGuard {
```

Standalone contract. Does not inherit `BaseYieldPoolBond`.

```
L69–L73   enum BeliefTier { Supporter, Believer, Champion }
```

Three tiers determined at stake time by msg.value thresholds.

```
L77–L98   struct Bond { bondId, staker, builder, builderName, projectDescription, stakeAmount, createdAt, vestingUntil, active }
L99–L109  struct BuildingMetrics { timestamp, submitter, codeCommits, deployments, usersServed, openSourceScore, innovationScore, transactionVolume, buildingNotes }
L101–L110 struct CommunityVerification { verifier, timestamp, confirmsBuilding, confirmsUsefulProject, confirmsOpenSource, relationship, notes }
L111–L118 struct Distribution { timestamp, totalAmount, builderShare, stakersShare, builderFundShare, reason }
```

Four structs model the bond lifecycle. `totalAmount` is `int256` to accommodate negative appreciation.

```
L122      uint256 public nextBondId = 1;
L123      mapping(uint256 => Bond) public bonds;
L124      mapping(uint256 => BuildingMetrics[]) public bondMetrics;
L125      mapping(uint256 => CommunityVerification[]) public bondVerifications;
L126      mapping(uint256 => Distribution[]) public bondDistributions;
L127      mapping(uint256 => mapping(address => uint256)) public stakerShares;
L129      uint256 public builderFund;
L132      uint256 public constant BELIEVER_THRESHOLD = 0.001 ether;
L133      uint256 public constant CHAMPION_THRESHOLD = 0.01 ether;
L136      uint256 public constant SUPPORTER_VESTING = 7776000;   // ~90 days
L137      uint256 public constant BELIEVER_VESTING  = 15552000;  // ~180 days
L138      uint256 public constant CHAMPION_VESTING  = 31536000;  // ~365 days
L141      uint256 public constant BUILDING_THRESHOLD = 4000;
L142      uint256 public constant TRANSACTION_PENALTY = 5000;
```

### State Variables

| Variable | Type | Description |
|---|---|---|
| `nextBondId` | `uint256` | Auto-incrementing bond ID counter |
| `bonds` | `mapping(uint256 → Bond)` | Bond storage keyed by ID |
| `bondMetrics` | `mapping(uint256 → BuildingMetrics[])` | Array of building metric submissions per bond |
| `bondVerifications` | `mapping(uint256 → CommunityVerification[])` | Community verification entries |
| `bondDistributions` | `mapping(uint256 → Distribution[])` | Historical distributions |
| `stakerShares` | `mapping(uint256 → mapping(address → uint256))` | Staker's initial contribution per bond |
| `builderFund` | `uint256` | Accumulated fund share; never dispersed (no withdrawal function) |
| `BELIEVER_THRESHOLD` | constant | 0.001 ETH — minimum for Believer tier |
| `CHAMPION_THRESHOLD` | constant | 0.01 ETH — minimum for Champion tier |
| `SUPPORTER_VESTING` | constant | 7 776 000 s ≈ 90 days |
| `BELIEVER_VESTING` | constant | 15 552 000 s ≈ 180 days |
| `CHAMPION_VESTING` | constant | 31 536 000 s ≈ 365 days |
| `BUILDING_THRESHOLD` | constant | 4 000 — below this building score triggers transacting penalty |
| `TRANSACTION_PENALTY` | constant | 5 000 — transaction volume above this deducts from building score |

---

### `createBond(builder, builderName, projectDescription) payable → uint256`

```
L179–L222

1. Require msg.value > 0
2. Require builder != address(0), builderName non-empty, projectDescription non-empty
3. Assign bondId = nextBondId++
4. Branch on msg.value vs CHAMPION_THRESHOLD / BELIEVER_THRESHOLD
   → vestingPeriod and tier assigned
5. Store Bond struct, vestingUntil = block.timestamp + vestingPeriod
6. stakerShares[bondId][msg.sender] = msg.value
7. Emit BondCreated, TierAchieved
8. Return bondId
```

`vestingUntil` is computed as `createdAt + period` where `createdAt = block.timestamp` at call time. No lock — staker can call `distributeBond` before `vestingUntil`; vesting is only used as a denominator in `vestingMultiplier` to reduce the payout multiplier.

---

### `buildingScore(bondId) → uint256`

```
L284–L302

commitScore  = min(codeCommits × 100, 10_000)
deployScore  = min(deployments × 1_000, 10_000)
userScore    = min(usersServed × 10, 10_000)

return (commitScore×30 + deployScore×30 + userScore×20 + openSourceScore×10 + innovationScore×10) / 100
```

Weighted average across five dimensions. Caps prevent any single dimension from dominating. Returns 0–10 000.

---

### `buildingVsTransacting(bondId) → uint256`

```
L308–L322

building = buildingScore(bondId)
if latest.transactionVolume > TRANSACTION_PENALTY (5000):
    penalty = (volume - 5000) / 100
    return building > penalty ? building - penalty : 0
return building
```

Every 100 units of transaction volume above 5 000 subtracts 1 point from the building score. High-frequency token flippers converge toward 0.

---

### `vestingMultiplier(bondId) → uint256`

```
L365–L376

vestedTime   = min(block.timestamp - createdAt, vestingUntil - createdAt)
totalVesting = vestingUntil - createdAt
vestedPercent = vestedTime × 100 / totalVesting

return 50 + vestedPercent    // range 50–150
```

At bond creation: 50 (0.5×). At full vest: 150 (1.5×). Linear between.

---

### `calculateBondValue(bondId) → uint256`

```
L396–L406

building     = buildingVsTransacting(bondId)    // 0–10 000
verification = communityVerificationMultiplier(bondId)  // 70–130
vesting      = vestingMultiplier(bondId)        // 50–150
time         = timeMultiplier(bondId)           // 100–250

return (stake × building × verification × vesting × time) / 100_000_000
```

**Math analysis at representative inputs:**

stake = 0.001 ETH = 1e15 wei  
building = 7 000, verification = 110, vesting = 100, time = 100  
→ `(1e15 × 7000 × 110 × 100 × 100) / 100_000_000`  
= `(1e15 × 7.7e9) / 1e8`  
= `7.7e24 / 1e8 = 7.7e16 wei ≈ 0.077 ETH`

That is a **77× return on a 0.001 ETH stake at year 0 before any time accrual** — the formula is massively miscalibrated. The divisor should be approximately 10 000 000 000 (1e10) to produce sensible par-value behaviour.

---

### `distributeBond(bondId)` — penalty inversion

```
L409–L475

building = buildingVsTransacting(bondId)

if building < BUILDING_THRESHOLD (4000):
    // "Transacting penalty"
    100% → builder          ← BUG: penalty goes to misbehaving party
else:
    60% → builder
    30% → staker
    10% → builderFund

payable(bond.builder).transfer(builderShare)   ← deprecated
payable(bond.staker).transfer(stakersShare)    ← deprecated
builderFund += fundShare                       ← locked forever, no withdrawal
```

The "transacting penalty" inverts the intended punishment: a builder who is merely transacting (bad) receives 100 % rather than 0 %. The design comment suggests the intent was "if transacting, only builder receives — stakers get nothing" — which is still not a penalty on the builder. V2 preserves the same inversion.

---

### Events

| Event | Arguments | Trigger |
|---|---|---|
| `BondCreated` | `bondId, staker, builder` | `createBond` |
| `BuildingMetricsSubmitted` | `bondId, submitter` | `submitBuildingMetrics` |
| `CommunityVerificationAdded` | `bondId, verifier` | `addCommunityVerification` |
| `BondDistributed` | `bondId, builderShare, stakersShare` | `distributeBond` |
| `TransactingPenalty` | `bondId, reason` | `distributeBond` when penalty |
| `TierAchieved` | `bondId, staker, tier` | `createBond` |

---

### Design Intent

Reward genuine builders with belief-backed capital that appreciates as they build. Anti-flipping vesting locks stakers in for 90–365 days. Community verification from real users adds social proof to the financial model.

### Why It Didn't Make Production

Superseded by V2. Key remaining issues: `.transfer()` usage, no timelock, no yield pool, and the formula miscalibration (divisor off by ~100×). V2 corrects the ETH transfer pattern but drops community verification from the value formula and changes the divisor in a way that doesn't fully correct the calibration.

---

---

# 29. AIPartnershipBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/AIPartnershipBonds.sol`
**Lines:** 524
**Inherits:** `ReentrancyGuard` + `MissionEnforcement` (standalone)

## What It Was Supposed to Do

Formalises an economic bond between a human and an AI agent. The human stakes ETH; the AI agent co-participates (holds a wallet address). The bond appreciates when the human grows — learning new skills, increasing autonomy, mastering tasks the AI previously performed. If the AI is dominating (low quality, declining autonomy, insufficient human verification), a 100 % human penalty applies and the AI receives nothing. Normal split is 50 % human, 30 % AI, 20 % partnership fund.

## Why It Was Likely Deprecated

1. **"AI agent" is just an Ethereum address** — no actual AI enforceability. Any wallet can be the "AI agent."
2. **`.transfer()` deprecated** on both `payable(bond.human)` and `payable(bond.aiAgent)`.
3. **Partnership fund has a `donateToPartnershipFund()` entry** but no withdrawal — funds accumulate and are permanently locked.
4. **Metrics are self-reported** — both human and AI can submit `humanGrowth=10000` with no oracle.
5. **`shouldActivateAIDominationPenalty`** — the verification check is `if verification <= 70` which means the neutral default (100) passes. But a contract with 0 verifications returns 100, so the check never fires unless someone explicitly submits all-negative verifications. Design intent and implementation do not align.
6. **No timelock, no yield pool**.

---

## Full Source with Line Numbers

```
L1–L5    SPDX, pragma ^0.8.25, ReentrancyGuard + MissionEnforcement
L17      contract AIPartnershipBonds is ReentrancyGuard {
```

---

### Structs

```
L69–L77   Bond { bondId, human, aiAgent, partnershipType, stakeAmount, createdAt, active }
L79–L88   PartnershipMetrics { timestamp, submitter, humanGrowth, humanAutonomy, humanDignity, tasksMastered, creativityScore, progressNotes }
L90–L98   HumanVerification { verifier, timestamp, confirmsPartnership, confirmsGrowth, confirmsAutonomy, relationship, notes }
L100–L107 Distribution { timestamp, totalAmount, humanShare, aiShare, partnershipFundShare, reason }
```

`totalAmount` is `int256` to allow negative appreciation.

---

### State Variables

| Variable | Type | Description |
|---|---|---|
| `AI_PROFIT_CAP` | constant `30` | Maximum AI share percentage of proceeds |
| `nextBondId` | `uint256` | Auto-increment |
| `bonds` | `mapping(uint256 → Bond)` | Bond storage |
| `bondMetrics` | `mapping(uint256 → PartnershipMetrics[])` | Partnership metric entries |
| `bondVerifications` | `mapping(uint256 → HumanVerification[])` | Human verifier entries |
| `bondDistributions` | `mapping(uint256 → Distribution[])` | Historical distributions |
| `partnershipFund` | `uint256` | Accumulated 20 % fund shares — no withdrawal |
| `PARTNERSHIP_QUALITY_THRESHOLD` | constant `4000` | Below = AI domination penalty |
| `DECLINING_AUTONOMY_THRESHOLD` | constant `3000` | Below = AI takeover |

---

### `createBond(aiAgent, partnershipType) payable → uint256`

```
L158–L180

1. Require msg.value > 0
2. Require aiAgent != address(0)
3. Require aiAgent != msg.sender (AI and human must differ)
4. Require partnershipType non-empty
5. bondId = nextBondId++
6. Store Bond struct (human = msg.sender, aiAgent = aiAgent)
7. Emit BondCreated
8. Return bondId
```

Both human and AI can later submit metrics via `submitPartnershipMetrics()` — there is no asymmetry between who reports what.

---

### `partnershipQualityScore(bondId) → uint256`

```
L250–L271

cutoff = block.timestamp - 7776000  (90-day window)

Loop backwards through bondMetrics while entry.timestamp >= cutoff:
    score = (humanGrowth + humanAutonomy + humanDignity + creativityScore) / 4
    totalScore += score
    count++

return count > 0 ? totalScore / count : 5000
```

Rolling 90-day average of the four human-centric dimensions. `tasksMastered` is not included here — it feeds the mastery multiplier separately.

---

### `humanMasteryMultiplier(bondId) → uint256`

```
L278–L290

totalTasksMastered = sum of all metrics[i].tasksMastered
multiplier = 100 + (totalTasksMastered × 10)
return min(multiplier, 200)
```

Cumulative over the bond's lifetime. Each task mastered adds 10 basis points (0.10×), capped at 200 (2.0×). No time decay — early task counts persist forever.

---

### `humanVerificationMultiplier(bondId) → uint256`

```
L297–L332

cutoff = block.timestamp - 15552000  (180-day window)

Loop backwards collecting recent verifications:
    recentCount++
    partnershipConfirmations += confirmsPartnership ? 1 : 0
    growthConfirmations      += confirmsGrowth ? 1 : 0
    autonomyConfirmations    += confirmsAutonomy ? 1 : 0

avgRate = average of three confirmation rates (0–100)

if avgRate >= 80: return 130
if avgRate >= 50: return 100
return 70 + (avgRate / 4)    // range 70–82 for 0–49
```

If no verifications exist, returns 100 (neutral). The low-end formula `70 + avgRate/4` gives a floor of 70 and a ceiling of ~82 for the weak band — never reaches 100 from below, creating a gap.

---

### `calculateBondValue(bondId) → uint256`

```
L352–L361

quality      = partnershipQualityScore(bondId)    // 0–10 000
mastery      = humanMasteryMultiplier(bondId)     // 100–200
verification = humanVerificationMultiplier(bondId) // 70–130
time         = timeMultiplier(bondId)             // 100–200

return (stake × quality × mastery × verification × time) / 100_000_000
```

**Math analysis:**

stake = 0.01 ETH = 1e16  
quality = 7 000, mastery = 110, verification = 100, time = 100  
→ `(1e16 × 7000 × 110 × 100 × 100) / 100_000_000`  
= `7.7e22 / 1e8 = 7.7e14 × 1e2 / 1e2 = 7.7e16 wei ≈ 0.077 ETH`

7× return at year 0 — same miscalibration pattern as AIAccountabilityBonds.

---

### `shouldActivateAIDominationPenalty(bondId) → (bool, string)`

Three conditions trigger the penalty:

1. `quality < 4000` — partnership quality too low
2. Latest `humanAutonomy < 3000` — autonomy declining
3. `humanVerificationMultiplier <= 70` — insufficient verification (note: 0 verifications returns 100, so this never fires on an unverified bond)

---

### `distributeBond(bondId)` — split and `.transfer()`

```
L404–L467

Caller must be human or AI agent.

if appreciation > 0:
    if penalty:
        human = 100%
        ai = 0%
    else:
        human = 50%
        ai    = 30%   // AI_PROFIT_CAP enforced
        fund  = 20%

if appreciation < 0:
    human = -appreciation (depreciation as compensation)

payable(bond.human).transfer(humanShare)    ← deprecated
payable(bond.aiAgent).transfer(aiShare)     ← deprecated
partnershipFund += fundShare                ← locked forever
```

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, human, aiAgent)` | `createBond` |
| `PartnershipMetricsSubmitted(bondId, submitter)` | `submitPartnershipMetrics` |
| `HumanVerificationAdded(bondId, verifier)` | `addHumanVerification` |
| `BondDistributed(bondId, humanShare, aiShare)` | `distributeBond` |
| `AIDominationPenalty(bondId, reason)` | `distributeBond` when penalty |
| `TasksMastered(bondId, taskCount)` | `submitPartnershipMetrics` when tasksMastered > 0 |

---

### Design Intent

Create a financial stake that makes AI's success contingent on the human partner genuinely learning and growing. AI earns only when the human is better off — measured by skill mastery, autonomy, and third-party social verification.

### Why It Didn't Make Production

No mechanism to verify AI agent identity. Metrics are self-reported by either party. The verification gate for the domination penalty effectively doesn't fire on unverified bonds. Partnership fund is permanently locked. `.transfer()` deprecated. The conceptual model is interesting but the implementation has no enforcement of the "AI" side's behavior.

---

---

# 30. FanVerificationSybilResistance.sol

**Path:** `theloopbreaker-site/contracts/deprecated/FanVerificationSybilResistance.sol`
**Lines:** 647
**Inherits:** Standalone (no OZ imports beyond the base pattern)

## What It Was Supposed to Do

Prevents fake or duplicate fan verifications in sports integrity bonds (CompetitiveIntegrityBond, TeamworkIntegrityBond). Fans register, stake ETH proportional to their reputation tier, submit verification of game attendance and team performance, and earn or lose that stake based on accuracy. Sybil resistance operates through three layers: stake-based skin in the game, NFT ticket hash uniqueness, and geographic hash caps (max 5 verifications per location per game).

## Why It Was Likely Deprecated

1. **External `this.` call in `_checkTierUpgrade`** — `this.getFanTier(fan)` makes an external call from within an internal function, incurring unnecessary gas and bypassing potential reentrancy guard coverage.
2. **No integration with current bond system** — CompetitiveIntegrityBond's `_calculateFanConsensusScore` is a stub that always returns 5 000; this contract was never wired in.
3. **Stake levels are stored in a mutable mapping** — owner can change stake requirements at any time, potentially retroactively affecting existing verifications.
4. **Reputation score formula is not normalised correctly** — `volumeScore = (verificationCount × 2000) / 100` at 100 verifications = 2 000; when combined with `accuracyScore` max 6 000, the total can reach 10 000 + `longevityScore` + `tierBonus` before penalties, making the 10 000 ceiling routinely reached by any moderately active fan.
5. **`partnershipFund`-equivalent issue** — `rewardPool` and `slashPool` accumulate ETH but only `rewardPool` has a `fundRewardPool()` entry; `slashPool` has no disbursement path.
6. **Standalone** — no inheritance from BaseYieldPoolBond.

---

## Full Source with Line Numbers

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
```

No OZ imports — all access control and reentrancy logic is custom.

---

### Enums

```
L21–L31   enum VerificationStatus { Pending, Validated, Invalid, Slashed, Disputed }
L32–L44   enum ReputationTier { New, Regular, Trusted, Verified, Expert }
```

Five verification statuses track a submission's lifecycle. Five reputation tiers map to descending stake requirements.

---

### Structs

```
L45–L60   FanProfile {
              address fan,
              uint256 registeredAt,
              uint256 totalVerifications,
              uint256 accurateVerifications,
              uint256 slashedVerifications,
              uint256 disputedVerifications,
              bool isBanned,
              ReputationTier tier
          }

L61–L77   Verification {
              uint256 id,
              address fan,
              uint256 gameId,
              bytes32 nftTicketHash,
              bytes32 geographicHash,
              uint256 teamPerformanceScore,
              uint256 playerEffortScore,
              uint256 attendanceConfidence,
              uint256 timestamp,
              VerificationStatus status,
              string notes
          }

L80–L87   NFTTicketValidation { bytes32 ticketHash, address fan, uint256 gameId, uint256 timestamp, bool isValid }
L91–L99   GeographicValidation { bytes32 locationHash, uint256 gameId, uint256 timestamp, bool isValid, string flagReason }
```

---

### State Variables

| Variable | Type | Description |
|---|---|---|
| `owner` | `address` | Admin; set at construction |
| `stakeLevels` | `mapping(ReputationTier → uint256)` | Required stake per tier |
| `fans` | `mapping(address → FanProfile)` | Registered fan profiles |
| `verifications` | `mapping(uint256 → Verification)` | All verification submissions |
| `nextVerificationId` | `uint256` | Auto-increment |
| `nftTickets` | `mapping(bytes32 → NFTTicketValidation)` | Track used ticket hashes |
| `geoValidations` | `mapping(bytes32 → GeographicValidation)` | Track location proofs |
| `gameLocationCounts` | `mapping(uint256 → mapping(bytes32 → uint256))` | Verifications per game per location |
| `verificationPool` | `uint256` | ETH staked by verifiers |
| `rewardPool` | `uint256` | ETH funded by external donors for rewards |
| `slashPool` | `uint256` | Accumulated slashed stakes — no disbursement |
| `MAX_VERIFICATIONS_PER_LOCATION` | constant `5` | Geographic Sybil cap |
| `MINIMUM_TICKET_AGE` | constant `24 hours` | NFT ticket must be at least 24 h old |
| `ACCURACY_REWARD_PCT` | constant `1000` bps | 10 % reward on top of stake return |
| `SLASH_PCT` | constant `10000` bps | 100 % stake forfeit on slash |

---

### Constructor — stake level initialisation

```
L208–L234

constructor() {
    owner = msg.sender;
    paused = false;

    stakeLevels[ReputationTier.New]      = 0.01 ether;
    stakeLevels[ReputationTier.Regular]  = 0.005 ether;
    stakeLevels[ReputationTier.Trusted]  = 0.003 ether;
    stakeLevels[ReputationTier.Verified] = 0.001 ether;
    stakeLevels[ReputationTier.Expert]   = 0.0005 ether;
}
```

Higher-reputation fans stake less — the system rewards reliability with lower capital requirements.

---

### `submitVerification(gameId, nftTicketHash, geographicHash, teamPerformanceScore, playerEffortScore, attendanceConfidence, notes) payable`

```
L236–L340

1. Require not paused, fan not banned
2. Register fan if not already registered
3. Require msg.value >= stakeLevels[profile.tier]
4. Check NFT ticket hash:
   a. Require ticket not already used
   b. Require ticket timestamp >= MINIMUM_TICKET_AGE
   c. Validate ticket — store NFTTicketValidation
5. Check geographic hash:
   a. Require gameLocationCounts[gameId][geoHash] < MAX_VERIFICATIONS_PER_LOCATION
   b. Increment gameLocationCounts
   c. Store GeographicValidation
6. Increment profile.totalVerifications
7. verificationPool += msg.value
8. Store Verification struct (status = Pending)
9. Emit VerificationSubmitted
10. Return verificationId
```

Three independent Sybil gates: stake, ticket uniqueness, geographic cap.

---

### `validateVerification(verificationId, isAccurate, reason)` — only authorised reviewer

```
L341–L418

1. Require caller is authorised reviewer
2. Require submission.status == Pending
3. submission.status = isAccurate ? Validated : Invalid

if isAccurate:
    profile.accurateVerifications++
    reward = stake + (stake × ACCURACY_REWARD_PCT / 10000)  // 10% reward
    verificationPool -= stake
    rewardPool       -= reward - stake  // reward portion from rewardPool
    payable(fan).transfer(stake + reward)    ← deprecated
    emit VerificationRewarded(...)
else:
    // Do not slash on first Invalid — only penalise profile
    profile.disputedVerifications++
    // Stake remains in verificationPool (no return)
    emit VerificationSlashed with reason "Invalid"

_checkTierUpgrade(fan)
```

Note: Invalid verifications do not slash the stake — they leave it in the pool. Slashing (`VerificationSlashed` with SLASH_PCT=100 %) only fires via `validateVerification` calling it explicitly — the code is inconsistent; actual invalid case emits `VerificationSlashed` but does not call the internal `_slash` path for full forfeiture.

---

### `calculateReputationScore(address fan) → uint256`

```
L456–L487

accuracyRate   = (accurateVerifications × 10000) / totalVerifications
accuracyScore  = (accuracyRate × 6000) / 10000          // max 6000
volumeScore    = (min(totalVerifications, 100) × 2000) / 100   // max 2000
longevityScore = (min(daysSinceRegistration, 365) × 1000) / 365  // max 1000
tierBonus      = uint256(tier) × 200                    // max 800 (tier 4)

score = int256(accuracyScore + volumeScore + longevityScore + tierBonus)
score -= slashedVerifications × 2000
score -= disputedVerifications × 500

clamp to 0–10000
```

**Math analysis:** At 100 verifications with 100 % accuracy and 365 days tenure at Expert tier:

- `accuracyScore` = 6 000  
- `volumeScore` = 2 000  
- `longevityScore` = 1 000  
- `tierBonus` = 800 (4 × 200)  
- Sum = 9 800 before penalties

The score is clamped to 10 000, so a highly active fan reaches near-max quickly regardless of penalties unless they have 4+ slashes.

---

### `getFanTier(address fan) → ReputationTier` — external function with `this.` anti-pattern

```
L495–L520

if totalVerifications < 5: return New
accuracyRate = (accurateVerifications × 10000) / totalVerifications
if totalVerifications >= 100 && accuracyRate >= 9500: return Expert
if totalVerifications >= 50  && accuracyRate >= 8000: return Verified
if totalVerifications >= 20  && accuracyRate >= 7000: return Trusted
if totalVerifications >= 5:                           return Regular
return New
```

---

### `_checkTierUpgrade(fan)` — the external call anti-pattern

```
L628–L638

function _checkTierUpgrade(address fan) internal {
    FanProfile storage profile = fans[fan];
    ReputationTier currentTier = profile.tier;
    ReputationTier calculatedTier = this.getFanTier(fan);  ← external call
    if (uint256(calculatedTier) > uint256(currentTier)) {
        profile.tier = calculatedTier;
        emit ReputationTierUpgraded(fan, calculatedTier);
    }
}
```

`this.getFanTier(fan)` is an external `CALL` to the contract itself. It is completely unnecessary — `getFanTier` could be made `public` and called internally as `getFanTier(fan)` with zero extra gas. The `this.` pattern wastes 700+ gas on the external call overhead and is a widely-cited bad practice.

---

### Events

| Event | Trigger |
|---|---|
| `FanRegistered(fan, timestamp)` | First call to `submitVerification` |
| `ReputationTierUpgraded(fan, newTier)` | `_checkTierUpgrade` |
| `VerificationSubmitted(...)` | `submitVerification` |
| `VerificationValidated(...)` | `validateVerification` |
| `VerificationRewarded(...)` | Accurate validation |
| `VerificationSlashed(...)` | Invalid/slashed |
| `NFTTicketValidated(...)` | Ticket accepted |
| `NFTTicketFlagged(...)` | `flagNFTTicket` |
| `GeographicProofFlagged(...)` | `flagGeographicProof` |
| `FanBanned(fan, reason)` | `banFan` |
| `RewardPoolFunded(funder, amount)` | `receive()` or `fundRewardPool()` |

---

### Design Intent

Provide a comprehensive Sybil-resistance layer for fan-based verification that sports integrity bonds depend on. Real fans who attended games stake capital to confirm what they saw. Accurate reporters earn rewards; inaccurate ones lose stakes. Geographic and ticket caps prevent coordinated attacks.

### Why It Didn't Make Production

The integration target — `_calculateFanConsensusScore` in CompetitiveIntegrityBond — was never implemented (returns a hardcoded 5 000). The `this.getFanTier()` external call anti-pattern introduces unnecessary gas overhead. The `slashPool` permanently accumulates forfeited ETH with no governance disbursement path. `.transfer()` used for reward payouts.

---

---

# 31. VaultfireBugBounty.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VaultfireBugBounty.sol`
**Lines:** 611
**Inherits:** `ReentrancyGuard` (standalone)

## What It Was Supposed to Do

An on-chain bug bounty management system for the Vaultfire protocol. AI companies, researchers, and security auditors can submit vulnerability reports. Authorised reviewers assess severity, validate submissions, and award ETH payouts from the bounty pool. Submissions are hashed for privacy (details kept off-chain). Rate limiting prevents spam. Researcher reputation tracks reliability over time.

## Why It Was Likely Deprecated

1. **Requires 100 ETH minimum pool balance** — operational bootstrapping requirement is extremely high.
2. **Reward amounts are hardcoded constants** — Critical = 100 ETH, High = 25 ETH. No adjustability without redeployment.
3. **Hash-based privacy insufficient** — `keccak256(details)` only proves the details existed at submission time; no ZK proof or encryption. Details are off-chain with no on-chain retrieval.
4. **Reviewer authorisation centralised** — owner adds/removes reviewers with no timelock or multisig.
5. **Rate limiting uses separate daily counter variables** — `lastSubmissionDate` stores block timestamp, so comparison `today == lastDay` where `today = block.timestamp / 86400` is susceptible to miner timestamp manipulation within the 15-second window.
6. **`.call{}` used correctly** but bounty pool withdrawal has no second-step confirmation.
7. **Duplicate detection is manual** — reviewers must manually mark duplicates; no on-chain hash comparison against prior submissions.
8. **`MINIMUM_POOL_BALANCE` enforced at review time** — if the pool drops below 100 ETH, no new reviews can proceed, freezing all pending submissions.

---

## Full Source with Line Numbers

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    
L4    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
```

---

### Enums

```
L21–L31   enum Severity { None, Informational, Low, Medium, High, Critical }
L33–L44   enum Status { Submitted, UnderReview, Validated, Invalid, Duplicate, Fixed, Rewarded }
```

Six severity levels — `None` is the default/unset state. Seven lifecycle statuses track submission through triage to closure.

---

### Structs

```
L48–L65   Submission {
              uint256 id,
              address researcher,
              bytes32 detailsHash,        // keccak256 of off-chain vulnerability description
              Severity severity,
              Status status,
              uint256 submittedAt,
              uint256 reviewedAt,
              address reviewedBy,
              string internalNotes,
              bool isRewarded
          }

L66–L79   ResearcherProfile {
              uint256 totalSubmissions,
              uint256 validSubmissions,
              uint256 invalidSubmissions,
              uint256 duplicateSubmissions,
              uint256 totalEarned,
              uint256 highestSeverityFound,
              bool isBanned
          }
```

---

### State Variables

| Variable | Type | Description |
|---|---|---|
| `owner` | `address` | Admin |
| `pendingOwner` | `address` | Two-step ownership pending |
| `authorizedReviewers` | `mapping(address → bool)` | Reviewer whitelist |
| `submissions` | `mapping(uint256 → Submission)` | All bug reports |
| `researchers` | `mapping(address → ResearcherProfile)` | Researcher history |
| `nextSubmissionId` | `uint256` | Auto-increment from 1 |
| `bountyPool` | `uint256` | Tracks ETH available for rewards (accounting variable) |
| `MINIMUM_POOL_BALANCE` | constant `100 ether` | Pool floor below which reviewing is blocked |
| `CRITICAL_REWARD` | constant `100 ether` | Critical bug payout |
| `HIGH_REWARD` | constant `25 ether` | High severity payout |
| `MEDIUM_REWARD` | constant `5 ether` | Medium severity payout |
| `LOW_REWARD` | constant `1 ether` | Low severity payout |
| `MAX_SUBMISSIONS_PER_DAY` | constant `5` | Daily rate limit per researcher |
| `lastSubmissionTime` | `mapping(address → uint256)` | Cooldown tracking (1-hour minimum) |
| `submissionsToday` | `mapping(address → uint256)` | Daily count |
| `lastSubmissionDate` | `mapping(address → uint256)` | Timestamp of last submission date reset |

---

### `fundBountyPool() payable` — public

```
L203–L222

Require msg.value > 0
bountyPool += msg.value
Emit BountyPoolFunded(funder, amount, newBalance)
```

Anyone can fund the bounty pool. The contract's actual ETH balance is `address(this).balance`; `bountyPool` is a separate accounting variable that tracks expected availability. These can diverge if ETH is sent directly via `receive()`.

---

### `submitVulnerability(detailsHash, severity, internalNotes) → uint256`

```
L224–L275

1. Require researcher not banned
2. Require not paused
3. _checkRateLimit(researcher) — 1-hour cooldown + daily cap of 5
4. _updateDailySubmissions(researcher)
5. severity must not be None
6. nextSubmissionId++ → id
7. Store Submission { status=Submitted, severity, detailsHash }
8. researchers[researcher].totalSubmissions++
9. Emit SubmissionCreated(id, researcher, severity)
10. Return id
```

**Privacy model:** `detailsHash = keccak256(description)`. The actual vulnerability text is off-chain. On-chain, only the hash is stored — it acts as a commitment but provides no confidentiality guarantee against the submitter or reviewer sharing the plaintext.

---

### `reviewSubmission(submissionId, newStatus, internalNotes)` — only reviewer

```
L276–L320

1. Require caller in authorizedReviewers
2. Require status == Submitted (only open submissions can be reviewed)
3. submission.status = newStatus
4. submission.reviewedAt = block.timestamp
5. submission.reviewedBy = msg.sender

Reputation adjustments:
  if Validated:    profile.validSubmissions++
  if Invalid:      profile.invalidSubmissions--  ← note: decrement, not increment
  if Duplicate:    profile.duplicateSubmissions++

6. Emit SubmissionReviewed
```

**Bug:** `profile.invalidSubmissions--` (decrement) is used where `++` was clearly intended. This causes an underflow revert if `invalidSubmissions == 0`, which it always is for a new researcher's first invalid submission. This would prevent any first-time invalid review from completing.

---

### `awardBounty(submissionId)` — only reviewer, nonReentrant

```
L321–L358

1. Require caller in authorizedReviewers
2. Require submission.status == Validated
3. Require !submission.isRewarded
4. reward = _calculateReward(submission.severity)
5. Require bountyPool >= reward (accounting check)
6. Require address(this).balance >= reward (balance check)
7. Require bountyPool >= MINIMUM_POOL_BALANCE (floor check — post-award pool)

   Note: the minimum balance check uses the pre-deduction bountyPool value,
   meaning the check is: bountyPool >= MINIMUM_POOL_BALANCE rather than
   (bountyPool - reward) >= MINIMUM_POOL_BALANCE

8. bountyPool -= reward
9. submission.isRewarded = true
10. profile.totalEarned += reward
11. Update profile.highestSeverityFound if severity > current
12. (bool ok, ) = payable(researcher).call{value: reward}("")
    Require ok, "transfer-failed"
13. Emit BountyAwarded
```

**MINIMUM_POOL_BALANCE check placement:** The check `require(bountyPool >= MINIMUM_POOL_BALANCE)` fires before the `bountyPool -= reward` deduction. This means a pool of exactly 100 ETH awarding a 100 ETH Critical bounty would pass (100 >= 100) and then leave `bountyPool = 0`, violating the spirit of the minimum balance floor.

---

### `calculateReputationScore(address researcher) → uint256`

```
L496–L522

score = 5000 (neutral start)
score += validSubmissions × 1000
score -= invalidSubmissions × 200
score -= duplicateSubmissions × 100
if highestSeverityFound == Critical: score += 2000
if highestSeverityFound == High:     score += 1000
clamp to 0–10000
```

Straightforward integer reputation model. `validSubmissions × 1000` means 5 valid bugs raises score to maximum — highly sensitive to valid count.

---

### `_checkRateLimit(researcher)` — internal

```
L571–L590

Require block.timestamp >= lastSubmissionTime[researcher] + 1 hour
today = block.timestamp / 1 days
lastDay = lastSubmissionDate[researcher] / 1 days
if today == lastDay && submissionsToday[researcher] >= 5:
    revert("Daily submission limit reached")
```

Day boundary computed by integer division of `block.timestamp / 86400`. Susceptible to timestamp manipulation at day boundaries but practically acceptable given the 15-second EVM timestamp window.

---

### Ownership — two-step transfer

```
L448–L469

transferOwnership(newOwner):
    pendingOwner = newOwner
    Emit OwnershipTransferInitiated

acceptOwnership():
    Require msg.sender == pendingOwner
    owner = pendingOwner
    pendingOwner = address(0)
    Emit OwnershipTransferred
```

Correct two-step ownership pattern. No timelock on the transfer itself — owner can nominate instantly; new owner can accept after deployment confirms.

---

### Events

| Event | Trigger |
|---|---|
| `BountyPoolFunded(funder, amount, balance)` | `fundBountyPool` |
| `BountyPoolWithdrawn(owner, amount, balance)` | `withdrawBountyPool` |
| `SubmissionCreated(id, researcher, severity)` | `submitVulnerability` |
| `SubmissionReviewed(id, reviewer, newStatus)` | `reviewSubmission` |
| `BountyAwarded(id, researcher, severity, amount)` | `awardBounty` |
| `ResearcherBanned/Unbanned(researcher, reason)` | `banResearcher`/`unbanResearcher` |
| `ReviewerAdded/Removed(reviewer)` | `addReviewer`/`removeReviewer` |
| `OwnershipTransferInitiated(old, new)` | `transferOwnership` |
| `OwnershipTransferred(old, new)` | `acceptOwnership` |

---

### Design Intent

On-chain bug bounty management that provides an immutable submission record (via hash), transparent reward amounts, and researcher reputation tracking — all without requiring vulnerability details to be published on-chain.

### Why It Didn't Make Production

Critical bugs: (1) `profile.invalidSubmissions--` underflow reverts on first invalid review; (2) MINIMUM_POOL_BALANCE check does not account for the award deduction. Operational issues: 100 ETH minimum pool requirement is impractical for a new protocol. Hardcoded reward constants require redeployment to adjust. The hash-only privacy model provides no real confidentiality.

---

---

# 32. VaultfireLoyalty.sol

**Path:** `theloopbreaker-site/contracts/deprecated/VaultfireLoyalty.sol`
**Lines:** 81
**Pragma:** `^0.8.19`
**Inherits:** `ERC20` + `EIP712`

## What It Was Supposed to Do

Mint one loyalty token (VFLOYAL, 1e18 units) to any address that presents a valid EIP-712 signed `XPMessage` with XP at or above the configured threshold. The signature must come from the user themselves — the signer is verified to equal `msgData.account`, which equals `msg.sender`. Admin can reset claim status and adjust the XP threshold.

## Why It Was Likely Deprecated

1. **Self-signed EIP-712 — the signature is meaningless** — `require(signer == msgData.account)` where `msgData.account == msg.sender`. The user signs their own XP message and presents it to themselves. Anyone can generate their own signature for any XP value, bypassing the threshold entirely by simply signing an inflated number. There is no trusted issuer of the XP message.
2. **XP value is completely unconstrained** — nothing prevents a user from signing `xp = type(uint256).max`. The check `require(msgData.xp >= xpThreshold)` can always be satisfied by inflating the signed XP.
3. **One claim per address** — the `claimed[msg.sender] = true` flag is the only meaningful gate, but it can be reset by admin via `reset(account)`.
4. **`pragma ^0.8.19`** — lower than the rest of the codebase (most are `^0.8.25`). Inconsistency suggests it was written at a different time.
5. **No access control beyond `admin`** — single admin address with no timelock or multisig.
6. **Token is named "Vaultfire Loyalty" with symbol "VFLOYAL"** — conflicts with the role of `VaultfireRewardStream` which also provides reward tokens; unclear which is canonical.

---

## Full Source with Line Numbers

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.19;
L3    
L4    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
L5    import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
L6    import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
```

Lines 1–6: License, compiler version (0.8.19 — older than canonical system), and three OZ imports: ERC20 fungible token, EIP712 typed-data hashing, and ECDSA signature recovery.

```
L13   contract VaultfireLoyalty is ERC20, EIP712 {
```

Single contract inheriting both ERC20 and EIP712.

```
L14–L17   struct XPMessage {
              address account;
              uint256 xp;
          }
```

The signed payload. Only two fields: the claimant address and their XP value. No nonce, no expiry, no issuer address.

```
L19–L21   bytes32 private constant XP_MESSAGE_TYPEHASH =
              keccak256("XPMessage(address account,uint256 xp)");
```

EIP-712 type hash for `XPMessage`. Correct EIP-712 encoding — but the security flaw is not in the encoding, it is in who signs the message.

```
L23       address public admin;
L24       uint256 public xpThreshold;
L25       mapping(address => bool) public claimed;
```

Three state variables. `admin` has exclusive power to reset claims and change the threshold. No two-step admin transfer.

```
L30–L33   constructor(uint256 _xpThreshold) ERC20("Vaultfire Loyalty", "VFLOYAL") EIP712("VaultfireLoyalty", "1") {
              admin = msg.sender;
              xpThreshold = _xpThreshold;
          }
```

Constructor sets deployer as admin and initialises the XP threshold. Domain separator is `"VaultfireLoyalty"` version `"1"`.

```
L45–L61   function claim(XPMessage calldata msgData, bytes calldata signature) external {
              require(msgData.account == msg.sender, "wrong signer");
              require(!claimed[msg.sender], "already claimed");
              require(msgData.xp >= xpThreshold, "xp below threshold");
```

Lines 46–48: Three pre-conditions.

- L46: `msgData.account == msg.sender` — ensures the message is addressed to the caller.
- L47: `!claimed[msg.sender]` — one-time gate.
- L48: `msgData.xp >= xpThreshold` — XP must meet threshold.

```
              bytes32 structHash = keccak256(abi.encode(
                  XP_MESSAGE_TYPEHASH,
                  msgData.account,
                  msgData.xp
              ));
              bytes32 digest = _hashTypedDataV4(structHash);
              address signer = ECDSA.recover(digest, signature);
              require(signer == msgData.account, "invalid signature");
```

Lines 50–57: EIP-712 signature recovery. `_hashTypedDataV4` prefixes the struct hash with the domain separator. `ECDSA.recover` extracts the signer address. The requirement `signer == msgData.account` ensures the message was signed by the account — but since `msgData.account == msg.sender`, this means the user signs their own message. Anyone can do this at will with any XP value.

```
              claimed[msg.sender] = true;
              _mint(msg.sender, 1 ether);
              emit Claimed(msg.sender, msgData.xp);
          }
```

Lines 59–61: Mark claimed, mint 1 token (1e18 smallest unit), emit event. Emits the claimed `xp` value for off-chain record-keeping.

```
L67–L71   function reset(address account) external onlyAdmin {
              claimed[account] = false;
              emit Reset(account);
          }
```

Admin can re-enable claiming for any address. Combined with the self-signing vulnerability, this means the admin can grant effectively unlimited claims over time.

```
L75–L79   function setXpThreshold(uint256 newThreshold) external onlyAdmin {
              xpThreshold = newThreshold;
              emit XpThresholdUpdated(newThreshold);
          }
```

Admin can change the threshold with immediate effect. No timelock.

---

### State Variables Summary

| Variable | Description |
|---|---|
| `admin` | Single admin with claim reset and threshold powers |
| `xpThreshold` | Minimum XP required in signed message |
| `claimed` | One-time claim flag per address |
| `XP_MESSAGE_TYPEHASH` | EIP-712 type hash constant |

---

### `claim` — The Security Model

The intended design was presumably: **a trusted backend signs XP messages for users** based on verified off-chain activity, and users submit those signatures to claim tokens. This is a standard pattern (e.g., merkle airdrops or signed claims).

The implementation diverges: there is no `trustedSigner` or `backendAddress` variable. The recovered `signer` is compared to `msgData.account` (the user), not to a protocol-controlled key. This means the XP system has no trusted issuer — it is purely self-attestation.

**Correct implementation** would replace:
```
require(signer == msgData.account, "invalid signature");
```
with:
```
require(signer == trustedBackend, "invalid signature");
```
where `trustedBackend` is a protocol-controlled key that issues signed XP certificates off-chain.

---

### Events

| Event | Arguments | Trigger |
|---|---|---|
| `Claimed(account, xp)` | Account and XP at claim time | `claim` |
| `Reset(account)` | Account whose claim was reset | `reset` |
| `XpThresholdUpdated(newThreshold)` | New threshold value | `setXpThreshold` |

---

### Design Intent

Reward protocol participants who have accumulated XP (experience points in off-chain Vaultfire engagement) with a transferable loyalty token. EIP-712 structured data signing provides a clean user experience (compatible with MetaMask's `eth_signTypedData`).

### Why It Didn't Make Production

The core security model is broken: users self-sign their own XP claims with any value they choose. The contract does not enforce any trusted issuer. Any address can mint the loyalty token by signing a message with `xp = type(uint256).max`. This would need to be redesigned with a backend signing key before deployment.

---

---

# 33. HealthCommonsBonds.sol

**Path:** `theloopbreaker-site/contracts/deprecated/HealthCommonsBonds.sol`
**Lines:** 436
**Inherits:** `ReentrancyGuard` + `MissionEnforcement` (standalone V1)

## What It Was Supposed to Do

V1 of the health commons bond. A company stakes ETH against its commitment to reduce pollution and improve community health outcomes. The contract tracks two separate data streams — `PollutionMetrics` (air/water/food quality) and `HealthOutcomes` (respiratory, cancer, chronic disease, life expectancy, community health scores) — submitted by the company and attested by community members. Bond value is tied to both improvement trends. If pollution rises or health declines, the company's bond is penalised.

## Why It Was Likely Deprecated

1. **Uint256 underflow risk in `pollutionReductionScore` and `healthImprovementScore`** — line 256 (and analogous health line): `return improvement > -4000 ? uint256(50 + improvement / 80) : 0`. When `improvement` is a negative `int256` in the range `[-4000, -1000)`, the expression `50 + improvement/80` can be negative. Casting a negative `int256` to `uint256` wraps to a very large number, causing overflow. For example, `improvement = -2000`: `50 + (-2000/80) = 50 - 25 = 25` — that is fine. But `improvement = -3999`: `50 + (-3999/80) = 50 - 49 = 1` — also fine. Wait — let us check boundary: `improvement = -4000` is caught by the `> -4000` guard and returns 0. At `improvement = -3999`: `50 + (-3999/80) = 50 - 49 = 1`. At `improvement = -1000` (boundary of stable range): `50 + (-1000/80) = 50 - 12 = 38`. The formula is actually safe within the range `(-4000, -1000)` because integer division rounds toward zero in Solidity. The real risk is if Solidity ever computes `50 + negative_int` as negative before the cast — but since `int256` subtraction here cannot underflow to below 0 in the range -4000 to 0, the cast risk was overstated. However, the code is still confusing and relies on implicit range assumptions that are not documented.
2. **Divisor is 1 000 000** — V2 changes to 100 000 000 (×100), dramatically reducing output values. V1's divisor produces bond values orders of magnitude larger than stake at mid-range inputs.
3. **No input validation** — scores are accepted without range checks (e.g., `airQualityScore = 999999` is accepted).
4. **No `whenNotPaused`** — no circuit breaker.
5. **No timelock on distribution**.
6. **No yield pool** — `distributeBond` uses `.transfer()` directly from contract balance.
7. **Community share has no direct transfer recipient** — `communityShare` is recorded but no per-capita transfer executes (funds remain in contract after company share is transferred).

---

## Full Source with Line Numbers

```
L1    // SPDX-License-Identifier: MIT
L2    pragma solidity ^0.8.25;
L3    
L4    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
L5    import "../MissionEnforcement.sol";
```

```
L17   contract HealthCommonsBonds is ReentrancyGuard {
```

Standalone, no `BaseYieldPoolBond`.

---

### Structs

```
L69–L77   Bond { bondId, company, affectedRegion, stakeAmount, createdAt, active }
L79–L86   PollutionMetrics { timestamp, airQualityScore, waterPurityScore, foodSafetyScore }
L88–L96   HealthOutcomes { timestamp, respiratoryHealthScore, cancerHealthScore, chronicDiseaseScore, lifeExpectancyScore, communityHealthScore, affectedPopulation }
L100–L106 CommunityAttestation { attestor, timestamp, confirmsPollutionReduction, confirmsHealthImprovement, notes }
L109–L115 Distribution { timestamp, totalAmount, communityShare, companyShare, perCapitaAmount, reason }
```

---

### State Variables

| Variable | Type | Description |
|---|---|---|
| `nextBondId` | `uint256` | Auto-increment |
| `bonds` | `mapping(uint256 → Bond)` | Bond storage |
| `bondPollutionData` | `mapping(uint256 → PollutionMetrics[])` | Pollution reading history |
| `bondHealthData` | `mapping(uint256 → HealthOutcomes[])` | Health outcome history |
| `bondAttestations` | `mapping(uint256 → CommunityAttestation[])` | Community attestations |
| `bondDistributions` | `mapping(uint256 → Distribution[])` | Distribution history |

No threshold constants are declared (unlike V2 which defines them explicitly). Threshold values are hardcoded inline in `shouldActivatePoisoningPenalty`: `pollution < 80 || health < 80 || community < 70`.

---

### `pollutionReductionScore(bondId) → uint256`

```
L230–L258 (approximate line range)

Require >= 2 pollution data entries.

initialAvg = (initial.air + initial.water + initial.food) / 3
latestAvg  = (latest.air + latest.water + latest.food) / 3
improvement = int256(latestAvg) - int256(initialAvg)

if improvement >= 3000:  return 150 + uint256((improvement - 3000) / 100)
if improvement >= 1000:  return 100 + uint256((improvement - 1000) / 40)
if improvement >= -1000: return 100
return improvement > -4000 ? uint256(50 + improvement / 80) : 0
```

**The cast analysis (line 256):**

The expression is: `uint256(50 + improvement / 80)`

- `improvement` is `int256`, range assumed `(-4000, -1000)` by this branch.
- `improvement / 80` is `int256` division (rounds toward zero): range `(-50, -12)`.
- `50 + improvement/80` is `int256` in range `(0, 38)` — always non-negative within the valid range.
- Therefore `uint256(...)` cast is safe within the intended range.

However: if `improvement` could somehow be `<= -4001` and the `improvement > -4000` guard is incorrect, the expression `50 + improvement/80` could be negative (e.g., `improvement = -10000`: `50 + (-125) = -75`), and `uint256(-75)` wraps to `2^256 - 75` — a catastrophic overflow. The guard prevents this in normal operation but relies on correct `int256` comparison semantics. The code is fragile — a well-designed implementation would use an explicit `int256` cast for the intermediate computation.

**The deeper issue:** Unlike V2 which adds this comment and uses the same pattern, V1 has no NatSpec explaining the invariants, making the safety argument hard to audit.

---

### `healthImprovementScore(bondId) → uint256`

Same piecewise formula applied to the five-dimensional health outcome average:

```
if improvement >= 2000:  return 150 + uint256((improvement - 2000) / 60)
if improvement >= 500:   return 100 + uint256((improvement - 500) / 30)
if improvement >= -500:  return 100
return improvement > -4000 ? uint256(50 + improvement / 80) : 0
```

Different threshold breakpoints than the pollution version (2000/500 vs 3000/1000), presumably because health scores change more slowly than pollution readings.

---

### `calculateBondValue(bondId) → uint256`

```
pollution  = pollutionReductionScore(bondId)    // 0–200+
health     = healthImprovementScore(bondId)     // 0–200+
community  = communityVerificationMultiplier(bondId)  // 50–150
time       = timeMultiplier(bondId)             // 100–200

return (stake × pollution × health × community × time) / 1_000_000
```

**Divisor calibration — V1 vs V2:**

V1 divisor: `1_000_000` (1e6)  
V2 divisor: `100_000_000` (1e8) — ×100 larger

At representative mid-values (pollution=100, health=100, community=100, time=100, stake=0.01 ETH=1e16):

V1: `(1e16 × 100 × 100 × 100 × 100) / 1e6 = 1e24 / 1e6 = 1e18 wei = 1 ETH`

That is a **100× return on a 0.01 ETH stake at baseline (no improvement, no degradation, year 0)**. The formula is massively miscalibrated. V2's ×100 correction brings this to 0.01 ETH — par value — which is the correct baseline.

---

### `distributeBond(bondId)` — community share is stuck

```
L355–L416

Require health data with affectedPopulation > 0.

if appreciation > 0:
    if penalty:
        communityShare = 100%
        companyShare   = 0%
    else:
        communityShare = 70%
        companyShare   = 30%

if appreciation < 0:
    communityShare = uint256(-appreciation)  // full depreciation to community

perCapitaAmount = communityShare / population  // stored in Distribution

payable(bond.company).transfer(companyShare)   ← deprecated

// communityShare is NEVER transferred — funds remain in the contract
```

The `perCapitaAmount` is computed and stored in the Distribution struct, but no loop distributes community shares to individual community members. There is no community registry. This is the same gap as PurchasingPowerBonds V1 — the per-capita calculation exists purely as a record-keeping artefact.

---

### `shouldActivatePoisoningPenalty(bondId) → bool`

```
pollution < 80 || health < 80 || community < 70
```

Any one of three conditions triggers the full penalty. The thresholds (80, 80, 70) are hardcoded inline — no named constants, making them invisible to external auditors scanning state variables.

---

### `communityVerificationMultiplier(bondId) → uint256`

```
if 0 attestations:      return 50     // penalty for no verification
if 0 recent (<180d):    return 70

avgRate = average of (pollutionConfirmations/count, healthConfirmations/count)

if avgRate >= 80: return 150
if avgRate >= 50: return 100
return 50 + avgRate / 2
```

V1 default of 50 (instead of V2's 100) means unverified bonds immediately suffer a 50 % multiplier reduction on bond value. This is a stricter penalty for unverified bonds than V2 applies.

---

### Events

| Event | Trigger |
|---|---|
| `BondCreated(bondId, company, affectedRegion)` | `createBond` |
| `PollutionDataSubmitted(bondId, timestamp)` | `submitPollutionData` |
| `HealthDataSubmitted(bondId, timestamp)` | `submitHealthData` |
| `CommunityAttestationAdded(bondId, attestor)` | `addCommunityAttestation` |
| `BondDistributed(bondId, communityShare, companyShare)` | `distributeBond` |

---

### Design Intent

Make environmental remediation profitable. Companies that reduce the pollution they cause and improve community health outcomes earn bond appreciation. Community members attest to real-world changes, providing social proof. Per-capita distribution to affected residents creates a direct compensation mechanism for historical harm.

### Why It Didn't Make Production

Three compounding issues eliminated this version: (1) community share is computed but never transferred — affected residents cannot claim funds; (2) divisor miscalibration produces absurd returns (100× at baseline); (3) `.transfer()` deprecated. V2 corrects all three: community share goes to a yield pool for distribution, divisor is ×100 larger, and `.call{}` replaces `.transfer()`. The cast fragility in `pollutionReductionScore` also migrated to V2, making it a known technical debt item inherited by the successor.

---

---

# Cross-Reference Index

## Contracts by Pattern

### V1 → V2 Pairs

| V1 | V2 | Key Changes in V2 |
|---|---|---|
| VerdantAnchorBonds | VerdantAnchorBondsV2 | BaseYieldPoolBond, `.call{}`, timelock |
| HealthCommonsBonds | HealthCommonsBondsV2 | Divisor ×100, BaseYieldPoolBond, `.call{}` |
| LaborDignityBonds | LaborDignityBondsV2 | Dignity floor, BaseYieldPoolBond, off-by-one bug introduced |
| EscapeVelocityBonds | EscapeVelocityBondsV2 | Simplified formula, BaseYieldPoolBond |
| CommonGroundBonds | CommonGroundBondsV2 | Simplified formula, BaseYieldPoolBond |
| PurchasingPowerBonds | PurchasingPowerBondsV2 | Timelock, BaseYieldPoolBond, `whenNotPaused` |
| BuilderBeliefBonds | BuilderBeliefBondsV2 | Divisor halved, BaseYieldPoolBond, verification dropped |
| VerdantAnchorBonds | VerdantAnchorBondsV2 | Greenwashing penalty inversion corrected in V2 |

### Infrastructure Contracts

| Contract | Role |
|---|---|
| VaultfireDAO | Governance — guardian token + Governor stack |
| RewardStream | ETH-based reward distribution (admin + governor roles) |
| VaultfireRewardStream | Token-based linear vesting stream (ERC20) |
| RewardMultiplier | Belief + loyalty score → basis-point multiplier |
| SwapGate | Token swap with KYC bypass for trusted IDs |
| YieldPoolHealthMonitor | Off-bond monitoring of yield pool reserves |
| VaultfireBugBounty | On-chain bug report registry with ETH bounties |
| VaultfireLoyalty | ERC20 loyalty token with XP-threshold claim |

### Provenance / Identity Contracts

| Contract | Role |
|---|---|
| GhostkeyAttribution | Immutable contributor metadata + royalty routing |
| CovenantFlame | Lightweight provenance anchor for Chapter 3 deployments |
| ContributorUnlockKey | ERC721 access token with optional hook callback |
| GhostkeyLoyaltyLock | ETH lock with linearly scaling loyalty multiplier |
| FreedomVow | One-time guardian vow ritual on top of BeliefOracle |

### Sports Integrity Contracts

| Contract | Role |
|---|---|
| CompetitiveIntegrityBond | Team-level season bond |
| TeamworkIntegrityBond | Player-level season bond |
| FanBeliefBond | Fan-staked integrity bond on players/teams |
| FanVerificationSybilResistance | Sybil-resistant fan verification layer |

---

## Known Bugs and Issues by Contract

| Contract | Issue | Severity |
|---|---|---|
| LaborDignityBondsV2 | Off-by-one in `workerVerifiedScore` loop | High — silently uses wrong attestation index |
| VaultfireBugBounty | `profile.invalidSubmissions--` underflow | High — reverts on first invalid review of any researcher |
| VaultfireBugBounty | `MINIMUM_POOL_BALANCE` check before deduction | Medium — floor check does not account for award |
| VaultfireLoyalty | Self-signed EIP-712 — users sign their own XP | Critical — entire auth model is bypassed |
| CompetitiveIntegrityBond | `_calculateFanConsensusScore` always returns 5 000 | High — stub, fan scoring not implemented |
| FanVerificationSybilResistance | `this.getFanTier()` unnecessary external call | Low — gas waste, bad practice |
| HealthCommonsBonds (V1) | `distributeBond` never transfers community share | High — community funds locked in contract |
| PurchasingPowerBonds (V1) | `distributeBond` never transfers worker share | High — workers never receive funds |
| AIAccountabilityBonds | Human share has no recipient — locked | High — same gap |
| All V1 standalones | `payable().transfer()` deprecated | Medium — 2300 gas limit, breaks smart contract recipients |
| BuilderBeliefBonds (both) | Transacting penalty pays misbehaving party | Medium — inversion of intent |
| SwapGate | `swapToSOL` does not execute cross-chain transfer | High — emits event only, no actual SOL movement |
| GhostkeyAttribution | `.transfer()` in `receive()` and `payRoyalty()` | Low — same gas limit issue |

---

## Deprecation Reasons by Category

### Replaced by V2 with BaseYieldPoolBond

VerdantAnchorBonds, HealthCommonsBonds, LaborDignityBonds, EscapeVelocityBonds, CommonGroundBonds, PurchasingPowerBonds, BuilderBeliefBonds — all V1 versions superseded by V2 variants that inherit `BaseYieldPoolBond`, use `_useYieldPool()` for distribution, add `DISTRIBUTION_TIMELOCK`, replace `.transfer()` with `.call{}`, and add `whenNotPaused`.

### Design Concept Unimplemented

CompetitiveIntegrityBond (fan consensus stub), FanVerificationSybilResistance (no bond integration), SwapGate (no cross-chain execution for SOL), AIAccountabilityBonds (human share never transferred), AIPartnershipBonds (partnership fund locked, AI identity unenforced).

### Critical Security Issues

VaultfireLoyalty (self-signed XP), VaultfireBugBounty (underflow on invalid review, pool floor miscalculation).

### Infrastructure Replaced / Evolved

RewardStream → absorbed into BaseYieldPoolBond pattern. VaultfireRewardStream → separate ERC20 streaming path. RewardMultiplier → multiplier logic moved closer to bond contracts. GhostkeyLoyaltyLock → standalone loyalty primitive superseded by more integrated approaches.

---

*End of Vaultfire Codex — Batch 8: Deprecated Archive*
