# 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 |

