# VaultfireKeyProtocol (VKP) — Technical Reference

**Codex Batch 5 — 17 Contracts**
**Solidity** `^0.8.26` · **License** MIT · **Network** Base / Avalanche / Arbitrum / Polygon

---

## Table of Contents

1. [VKP Architecture Overview](#vkp-architecture-overview)
2. [IVaultfireIdentity.sol](#1-ivaultfireidentitysol)
3. [IVaultfireBonds.sol](#2-ivaultfirebondssol)
4. [IVaultfireReputation.sol](#3-ivaultfirereputationsol)
5. [KeyVault.sol](#4-keyvaultsol)
6. [VKPFactory.sol](#5-vkpfactorysol)
7. [SessionKeyManager.sol](#6-sessionkeymanagersol)
8. [ScopedDelegation.sol](#7-scopeddelegationsol)
9. [DelegationRegistry.sol](#8-delegationregistrysol)
10. [AgentSLAEnforcer.sol](#9-agentslaenforcersol)
11. [AgentInsurancePool.sol](#10-agentinsurancepoolsol)
12. [AgentSafetyManager.sol](#11-agentsafetymanagersol)
13. [ComplianceRegistry.sol](#12-complianceregistrysol)
14. [ReputationDecay.sol](#13-reputationdecaysol)
15. [MockIdentityRegistry.sol](#14-mockidentityregistrysol)
16. [MockBondsRegistry.sol](#15-mockbondsregistrysol)
17. [MockReputationRegistry.sol](#16-mockreputationregistrysol)
18. [ReentrancyAttacker.sol](#17-reentrancyattackersol)
19. [Math Reference](#math-reference)
20. [Security Invariants](#security-invariants)
21. [Dependencies and Rebuild Recipe](#dependencies-and-rebuild-recipe)

---

## VKP Architecture Overview

The VaultfireKeyProtocol is a layered system of eight production contracts, three interfaces, three testing mocks, and one adversarial test helper, all operating at `^0.8.26`. Its purpose is to give every AI agent registered through ERC-8004 a complete on-chain trust stack: a spending-policy-enforced wallet, ephemeral session keys, delegation chains that can only narrow (never widen) permissions, SLA commitments backed by staked collateral, a pooled insurance mechanism, an automated kill switch tied to reputation scores, a regulatory compliance registry for EU AI Act attestations, and a time-decay model that prevents stale reputation from gaming the system.

### Layered Architecture

```
┌──────────────────────────────────────────────────────────┐
│                    VKPFactory                            │
│  (one-call deployer of per-agent KeyVaults)              │
└────────────────────┬─────────────────────────────────────┘
                     │ deploys
                     ▼
┌──────────────────────────────────────────────────────────┐
│                    KeyVault                              │
│  Policy: maxPerTx · maxHourly · maxDaily · emergFreeze   │
│  Allowlist: contract targets · emergency controls        │
└────┬───────────────┬──────────────────────────────────────┘
     │               │
     │ validated by  │ validated by
     ▼               ▼
┌───────────────┐ ┌──────────────────────────────────────┐
│ SessionKey    │ │ DelegationRegistry / ScopedDelegation │
│ Manager       │ │  (agent-to-agent authority chains)    │
└───────────────┘ └──────────────────────────────────────┘
     │                        │
     └──────────┬─────────────┘
                │ both depend on
                ▼
┌──────────────────────────────────────────────────────────┐
│              Trust Layer                                 │
│  AgentSLAEnforcer · AgentInsurancePool                   │
│  AgentSafetyManager · ComplianceRegistry · ReputDecay    │
└──────────────────────────────────────────────────────────┘
                │
                │ all consult
                ▼
┌──────────────────────────────────────────────────────────┐
│              Shared Interfaces                           │
│  IVaultfireIdentity (ERC-8004) · IVaultfireBonds         │
│  IVaultfireReputation                                    │
└──────────────────────────────────────────────────────────┘
```

### Flow: Agent Acts as User

1. An AI agent is registered on-chain via ERC-8004 (`IVaultfireIdentity`).
2. The agent (or the Factory on its behalf) deploys a `KeyVault` — a personal spending-policy enforcement contract.
3. For a scoped task, the agent creates a `SessionKey` in `SessionKeyManager`, granting a short-lived address only the minimum authority needed.
4. If the agent needs to sub-contract work, it creates a `Scope` in `ScopedDelegation` or a full `Delegation` in `DelegationRegistry`, both of which enforce progressive narrowing of permissions.
5. `DelegationRegistry` re-verifies delegate trust live against bond balance and reputation score; if either drops below threshold, the delegation auto-revokes.
6. `AgentSLAEnforcer` enforces the agent's public performance commitments; violations slash the staked collateral.
7. `AgentInsurancePool` provides third-party coverage — premiums priced by reputation tier.
8. `AgentSafetyManager` acts as the kill switch layer; `ReputationDecay` feeds live decayed scores back into this layer and into insurance tier pricing.
9. `ComplianceRegistry` records EU AI Act attestations, maintaining a regulatory audit trail that external auditors can verify on-chain.

---

## 1. IVaultfireIdentity.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireIdentity.sol`
**Role:** Interface — ERC-8004 identity gate used by every VKP contract to verify agent registration.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireIdentity — Interface for the Vaultfire Identity Registry
     5  /// @notice Minimal interface used by VKP to verify ERC-8004 agent registration.
     6  ///         Uses getAgent() to check registration — returns agentURI + metadata.
     7  ///         An agent is registered if getAgent() returns a non-empty agentURI.
     8  interface IVaultfireIdentity {
     9      /// @notice Get an agent's on-chain identity data
    10      /// @dev Returns empty agentURI if the agent is not registered.
    11      /// @param agentAddress The agent address to look up
    12      /// @return agentURI The agent's URI (JSON metadata string)
    13      /// @return active Whether the agent is active
    14      /// @return agentType The agent type string
    15      /// @return registeredAt Block number when registered
    16      function getAgent(address agentAddress)
    17          external
    18          view
    19          returns (
    20              string memory agentURI,
    21              bool active,
    22              string memory agentType,
    23              uint256 registeredAt
    24          );
    25  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 1–2 | MIT license; Solidity minimum 0.8.26 required. |
| 4–7 | NatSpec describes the registration check convention: a non-empty `agentURI` means registered. |
| 8 | `interface` keyword — no storage, no constructor. |
| 9–24 | Single function `getAgent(address)`. Returns four values; callers check `bytes(agentURI).length > 0` for registration. |

### Functions

#### `getAgent(address agentAddress) → (string agentURI, bool active, string agentType, uint256 registeredAt)`
- **Visibility:** `external view`
- **Purpose:** Single source of truth for ERC-8004 registration status. Every VKP contract calls this in a modifier or constructor guard. The `active` flag is consulted only in contracts where an inactive agent should be treated differently (e.g., `ScopedDelegation`, `AgentSLAEnforcer`); `KeyVault` only checks `agentURI` length.
- **Return convention:** If the address is not registered, `agentURI` is an empty string and all other fields are zero/false/empty.

### Security Invariants

- VKP registration checks are read-only calls to an external registry. A malicious registry can return `true` for any address. Production deployments must use the canonical ERC-8004 registry at the well-known deployment address.
- Empty-string check (`bytes(agentURI).length == 0`) is the universal registration sentinel across all VKP contracts — consistent but does not verify the URI is well-formed JSON.

---

## 2. IVaultfireBonds.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireBonds.sol`
**Role:** Interface — live bond registry ABI for cross-chain accountability verification (Base, Avalanche, Arbitrum, Polygon).

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireBonds — Interface for Vaultfire Accountability Bond contracts
     5  /// @notice Matches the LIVE ABIs on Base, Avalanche, Arbitrum, and Polygon.
     6  ///         Bonds are looked up by bondId. The bond struct includes the creator
     7  ///         (aiCompany) address, which VKP uses to verify the delegate owns the bond.
     8  interface IVaultfireBonds {
     9      /// @notice Get bond details by ID
    10      /// @dev Matches the live getBond(uint256) on all 4 chains
    11      function getBond(uint256 bondId)
    12          external
    13          view
    14          returns (
    15              address creator,        // aiCompany / bond creator address
    16              uint256 stakeAmount,
    17              uint256 createdAt,
    18              bool active
    19          );
    20  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 4–7 | Declares that this ABI must match the live bonds contract on four networks — a cross-chain compatibility contract. |
| 11–19 | `getBond(uint256)` returns the bond creator (`aiCompany`), the staked amount in wei, creation timestamp, and whether the bond is active. |

### Functions

#### `getBond(uint256 bondId) → (address creator, uint256 stakeAmount, uint256 createdAt, bool active)`
- **Visibility:** `external view`
- **Used by:** `DelegationRegistry.acceptDelegation()` and `_verifyTrustRequirements()`, `AgentSLAEnforcer` constructor (storage).
- **Key usage pattern:** Callers verify `creator == delegate && active && stakeAmount >= minRequired`.

### Security Invariants

- The bond check in `DelegationRegistry` is wrapped in a `try/catch` (see §8). If the call reverts (e.g., bond ID does not exist), the delegation is auto-revoked rather than leaving it active — fail-closed design.
- `stakeAmount` is a snapshot at query time; nothing prevents a delegate from withdrawing stake after acceptance. The `verifyTrust()` function exists to detect this and revoke in-flight delegations.

---

## 3. IVaultfireReputation.sol

**File:** `vaultfire-keyprotocol/contracts/IVaultfireReputation.sol`
**Role:** Interface — reputation score feed used for delegation gating, insurance pricing, and safety auto-triggers.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  /// @title IVaultfireReputation — Interface for Vaultfire Reputation Registry
     5  /// @notice Used by VKP to check reputation scores for delegation gating
     6  interface IVaultfireReputation {
     7      function getReputation(address agent)
     8          external
     9          view
     10         returns (uint256 score, uint256 totalRatings, uint256 lastUpdated);
    11  }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 6–11 | Minimal interface: one function, three return values. `score` is the canonical 0–100 trust score. |

### Functions

#### `getReputation(address agent) → (uint256 score, uint256 totalRatings, uint256 lastUpdated)`
- **score:** 0–100 trust score. 0 = untrusted, 100 = maximum trust.
- **totalRatings:** lifetime rating count (used by `ReputationDecay` for context).
- **lastUpdated:** timestamp — `ReputationDecay` uses this to seed its own `lastActivityAt`.

### Security Invariants

- Reputation calls are also wrapped in `try/catch` in `DelegationRegistry._verifyTrustRequirements()` — a failing external call cannot keep a delegation alive.
- `AgentInsurancePool` reads reputation only at policy-creation time to compute the risk tier; it does not re-check during the policy lifecycle. An agent can improve or worsen their reputation after purchasing insurance without policy repricing.

---

## 4. KeyVault.sol

**File:** `vaultfire-keyprotocol/contracts/KeyVault.sol`
**Role:** KeyVault — per-agent spending-policy enforcement contract.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5
     6  /// @title KeyVault — On-chain spending policy enforcement for AI agent wallets
     7  /// @author Vaultfire Protocol
     8  /// @notice Wraps an agent's wallet with enforced spending limits, contract
     9  ///         allowlists, and emergency freeze. Policies are on-chain — no prompt
    10  ///         injection or client-side bypass can override them.
     11 /// @dev Requires the owner to be registered in the Vaultfire Identity Registry (ERC-8004).
    12  contract KeyVault {
    13      // ═══ REENTRANCY GUARD ═══
    14
    15      uint256 private constant _NOT_ENTERED = 1;
    16      uint256 private constant _ENTERED = 2;
    16      uint256 private _reentrancyStatus = _NOT_ENTERED;
    17
    18      modifier nonReentrant() {
    19          require(_reentrancyStatus != _ENTERED, "ReentrancyGuard: reentrant call");
    20          _reentrancyStatus = _ENTERED;
    21          _;
    22          _reentrancyStatus = _NOT_ENTERED;
    23      }
    24
    25      // ═══ STRUCTS ═══
    26
    27      struct Policy {
    28          uint256 maxPerTx;           // Max native token per single transaction (wei)
    29          uint256 maxDaily;           // Max native token per rolling 24h window (wei)
    30          uint256 maxHourly;          // Max native token per rolling 1h window (wei)
    31          uint256 emergencyThreshold; // Freeze if total spent exceeds this (wei)
    32          bool frozen;                // Emergency freeze flag
    33      }
    34
    35      struct SpendingWindow {
    36          uint256 hourlySpent;
    37          uint256 dailySpent;
    38          uint256 totalSpent;
    39          uint256 hourlyResetAt;
    40          uint256 dailyResetAt;
    41      }
    42
    43      // ═══ STATE ═══
    44
    45      address public immutable owner;
    46      IVaultfireIdentity public immutable identityRegistry;
    47
    48      Policy public policy;
    49      SpendingWindow public spending;
    50
    51      mapping(address => bool) public allowedContracts;
    52      uint256 public allowedContractCount;
    53
    54      bool public allowlistEnabled;
    55
    56      // ═══ EVENTS ═══
    57
    58      event VaultCreated(address indexed owner, uint256 maxDaily, uint256 maxPerTx);
    59      event TransactionExecuted(address indexed to, uint256 value, bytes data);
    60      event PolicyUpdated(uint256 maxPerTx, uint256 maxDaily, uint256 maxHourly);
    61      event ContractAllowed(address indexed target);
    62      event ContractRemoved(address indexed target);
    63      event EmergencyFreeze(uint256 totalSpent, uint256 threshold);
    64      event EmergencyUnfreeze(address indexed owner);
    65      event FundsDeposited(address indexed from, uint256 amount);
    66      event FundsWithdrawn(address indexed to, uint256 amount);
    67
    68      // ═══ ERRORS ═══
    69
    70      error NotOwner();
    71      error NotRegistered();
    72      error VaultFrozen();
    73      error ExceedsPerTxLimit(uint256 requested, uint256 limit);
    74      error ExceedsHourlyLimit(uint256 wouldSpend, uint256 limit);
    75      error ExceedsDailyLimit(uint256 wouldSpend, uint256 limit);
    76      error ContractNotAllowed(address target);
    77      error TransactionFailed();
    78      error ZeroAddress();
    79
    80      // ═══ MODIFIERS ═══
    81
    82      modifier onlyOwner() {
    83          if (msg.sender != owner) revert NotOwner();
    84          _;
    85      }
    86
    87      modifier notFrozen() {
    88          if (policy.frozen) revert VaultFrozen();
    89          _;
    90      }
    91
    92      // ═══ CONSTRUCTOR ═══
    93
    94      constructor(
    95          address _owner,
    96          address _identityRegistry,
    97          uint256 _maxPerTx,
    98          uint256 _maxHourly,
    99          uint256 _maxDaily,
    100         uint256 _emergencyThreshold
    101     ) {
    102         if (_owner == address(0)) revert ZeroAddress();
    103         if (_identityRegistry == address(0)) revert ZeroAddress();
    104
    105         identityRegistry = IVaultfireIdentity(_identityRegistry);
    106
    107         // Verify owner is a registered ERC-8004 agent
    108         (string memory agentURI,,,) = identityRegistry.getAgent(_owner);
    109         if (bytes(agentURI).length == 0) revert NotRegistered();
    110
    110         owner = _owner;
    111         policy = Policy({
    112             maxPerTx: _maxPerTx,
    113             maxDaily: _maxDaily,
    114             maxHourly: _maxHourly,
    115             emergencyThreshold: _emergencyThreshold,
    116             frozen: false
    117         });
    118
    119         spending = SpendingWindow({
    120             hourlySpent: 0,
    120             dailySpent: 0,
    121             totalSpent: 0,
    122             hourlyResetAt: block.timestamp + 1 hours,
    123             dailyResetAt: block.timestamp + 1 days
    124         });
    125
    126         allowlistEnabled = false;
    127         emit VaultCreated(_owner, _maxDaily, _maxPerTx);
    128     }
    129
    130     // ═══ RECEIVE / DEPOSIT ═══
    131
    132     receive() external payable {
    133         emit FundsDeposited(msg.sender, msg.value);
    134     }
    135
    136     function deposit() external payable {
    137         emit FundsDeposited(msg.sender, msg.value);
    138     }
    139
    140     // ═══ EXECUTE ═══
    141
    142     function execute(
    143         address to,
    144         uint256 value,
    145         bytes calldata data
    146     ) external onlyOwner notFrozen nonReentrant returns (bytes memory result) {
    147         if (to == address(0)) revert ZeroAddress();
    148
    149         if (allowlistEnabled && !allowedContracts[to]) {
    150             revert ContractNotAllowed(to);
    151         }
    152
    153         if (value > policy.maxPerTx) {
    154             revert ExceedsPerTxLimit(value, policy.maxPerTx);
    155         }
    156
    157         _resetWindows();
    158
    159         if (spending.hourlySpent + value > policy.maxHourly) {
    160             revert ExceedsHourlyLimit(spending.hourlySpent + value, policy.maxHourly);
    160         }
    161
    162         if (spending.dailySpent + value > policy.maxDaily) {
    163             revert ExceedsDailyLimit(spending.dailySpent + value, policy.maxDaily);
    164         }
    165
    166         spending.hourlySpent += value;
    167         spending.dailySpent += value;
    168         spending.totalSpent += value;
    169
    170         if (policy.emergencyThreshold > 0 && spending.totalSpent > policy.emergencyThreshold) {
    171             policy.frozen = true;
    172             emit EmergencyFreeze(spending.totalSpent, policy.emergencyThreshold);
    173         }
    174
    175         emit TransactionExecuted(to, value, data);
    176
    177         bool success;
    178         (success, result) = to.call{value: value}(data);
    179         if (!success) revert TransactionFailed();
    180     }
    181
    182     // ═══ POLICY MANAGEMENT ═══
    183
    184     function updatePolicy(uint256 _maxPerTx, uint256 _maxHourly, uint256 _maxDaily,
    185         uint256 _emergencyThreshold) external onlyOwner { ... }
    186
    186     function allowContract(address target) external onlyOwner { ... }
    187     function removeContract(address target) external onlyOwner { ... }
    188     function disableAllowlist() external onlyOwner { ... }
    189     function enableAllowlist() external onlyOwner { ... }
    190
    191     // ═══ EMERGENCY ═══
    192
    192     function freeze() external onlyOwner { ... }
    193     function unfreeze() external onlyOwner { ... }
    194     function emergencyWithdraw() external onlyOwner nonReentrant { ... }
    195
    196     // ═══ VIEW ═══
    197
    196     function remainingHourly() external view returns (uint256) { ... }
    197     function remainingDaily() external view returns (uint256) { ... }
    198     function isFrozen() external view returns (bool) { ... }
    199     function balance() external view returns (uint256) { ... }
    200
    201     // ═══ INTERNAL ═══
    202
    202     function _resetWindows() internal { ... }
    203 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 15–26 | **Reentrancy guard** — inline implementation. `_NOT_ENTERED = 1`, `_ENTERED = 2`. Uses uint256 rather than bool to prevent gas optimisation that could create slot collision vulnerabilities. The `nonReentrant` modifier sets status to `_ENTERED` before body and resets after. |
| 27–33 | **`Policy` struct** — all spending policy fields. `frozen` flag doubles as the kill switch. `emergencyThreshold` is a lifetime total, not a window. |
| 35–41 | **`SpendingWindow` struct** — rolling window accumulators. `totalSpent` is the monotonically increasing lifetime total. |
| 45–46 | `owner` and `identityRegistry` are `immutable` — set in constructor, cannot be changed. No ownership transfer is possible on `KeyVault`. |
| 51–54 | `allowedContracts` mapping + `allowedContractCount` counter + `allowlistEnabled` flag. The count is used to auto-disable the allowlist when the last contract is removed. |
| 82–90 | `onlyOwner` uses a custom error `NotOwner()` (cheaper than a string revert). `notFrozen` checks `policy.frozen`. |
| 102–109 | Constructor: zero-address checks come first, then the ERC-8004 registration check. `getAgent()` is called with `_owner`; if the returned URI is empty, the constructor reverts — an unregistered address cannot own a `KeyVault`. |
| 119–127 | Spending windows initialized: hourly resets one hour from now, daily resets one day from now. `allowlistEnabled = false` — open by default. |
| 142–180 | **`execute()`** — the core enforcement function. Modifier order: `onlyOwner notFrozen nonReentrant`. Checks run in this order: (1) zero-address target, (2) allowlist gate, (3) per-tx limit, (4) window reset, (5) hourly limit, (6) daily limit. Effects (state updates) happen before the external call (CEI pattern). The `TransactionExecuted` event is emitted before `to.call` — an unusual pattern but not a security issue since the event is purely informational. |
| 166–168 | Spending accumulators updated before the external call. If `_resetWindows()` already zeroed a window, the fresh window starts accumulating with this transaction's value. |
| 170–173 | Emergency auto-freeze: after spending update, if `totalSpent > emergencyThreshold` and the threshold is non-zero, `policy.frozen` is set to `true` and `EmergencyFreeze` is emitted — all future `execute()` calls will revert with `VaultFrozen`. The current transaction still completes. |
| 232–265 | **`updatePolicy()`** — all four limits can be changed atomically. Note: increasing limits does not reset window counters; an agent could increase `maxDaily` mid-day to allow more spending. |
| 246–254 | **`allowContract()`** — idempotent. Only adds if not already present; sets `allowlistEnabled = true` upon first addition. |
| 257–266 | **`removeContract()`** — decrements count; when count reaches zero, auto-disables allowlist (open-world execution). |
| 283–286 | **`freeze()`** — manual kill switch. Emits `EmergencyFreeze` with current `totalSpent` and configured threshold for audit trail. |
| 289–293 | **`unfreeze()`** — resets `totalSpent` to zero so the emergency threshold accumulator starts fresh. |
| 296–301 | **`emergencyWithdraw()`** — full drain, `nonReentrant`. Emits `FundsWithdrawn` before the call (CEI). |
| 308–319 | View functions return snapshots: `remainingHourly()/remainingDaily()` check whether the window has already expired (returning full limit) without mutating state. |
| 335–344 | **`_resetWindows()`** — if `block.timestamp >= hourlyResetAt`, zeroes `hourlySpent` and advances `hourlyResetAt` by one hour. Same for daily. This is a fixed-point reset: it does not roll the window forward by one period per call — it always resets to `now + period`. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `owner` | `address immutable` | ERC-8004 agent that owns and controls this vault | Immutable |
| `identityRegistry` | `IVaultfireIdentity immutable` | Registry used for owner registration check at deploy time | Immutable |
| `policy` | `Policy storage` | Active spending policy including frozen flag | Mutable (owner) |
| `spending` | `SpendingWindow storage` | Rolling window accumulators | Mutable (execute path) |
| `allowedContracts` | `mapping(address => bool)` | Per-target allowlist | Mutable (owner) |
| `allowedContractCount` | `uint256` | Count of allowlisted contracts | Mutable (owner) |
| `allowlistEnabled` | `bool` | Master switch for allowlist enforcement | Mutable (owner) |
| `_reentrancyStatus` | `uint256 private` | Reentrancy guard state (1 = not entered, 2 = entered) | Mutable (execute path) |

### Functions

#### `execute(address to, uint256 value, bytes calldata data) → bytes result`
- **Modifiers:** `onlyOwner`, `notFrozen`, `nonReentrant`
- **Checks sequence:** zero address → allowlist → per-tx limit → window reset → hourly accumulator → daily accumulator → emergency threshold
- **Effects sequence:** hourly/daily/total spending updated → optional auto-freeze → event emitted
- **Interaction:** `to.call{value: value}(data)` — if this fails, reverts with `TransactionFailed()`

#### `updatePolicy(uint256, uint256, uint256, uint256)`
- **Modifier:** `onlyOwner`
- All four policy fields updated atomically. Does not reset window counters.

#### `allowContract(address target)` / `removeContract(address target)`
- **Modifier:** `onlyOwner`
- Idempotent add/remove. Auto-enables/disables allowlist by count.

#### `freeze()` / `unfreeze()`
- **Modifier:** `onlyOwner`
- `freeze()`: sets `policy.frozen = true`. `unfreeze()`: sets `false` AND resets `spending.totalSpent = 0`.

#### `emergencyWithdraw()`
- **Modifiers:** `onlyOwner`, `nonReentrant`
- Full drain of vault balance to owner address.

#### `_resetWindows()` internal
- Resets hourly/daily accumulators if their respective reset timestamps have passed.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `VaultCreated` | Constructor | `owner`, `maxDaily`, `maxPerTx` |
| `TransactionExecuted` | Before each `execute()` call | `to`, `value`, `data` |
| `PolicyUpdated` | `updatePolicy()` | `maxPerTx`, `maxDaily`, `maxHourly` |
| `ContractAllowed` | `allowContract()` on new entry | `target` |
| `ContractRemoved` | `removeContract()` on existing entry | `target` |
| `EmergencyFreeze` | Auto-threshold exceeded or manual `freeze()` | `totalSpent`, `threshold` |
| `EmergencyUnfreeze` | `unfreeze()` | `owner` |
| `FundsDeposited` | `receive()` or `deposit()` | `from`, `amount` |
| `FundsWithdrawn` | `emergencyWithdraw()` | `to`, `amount` |

### Security Invariants

1. **Policy cannot be bypassed by injection.** All checks run on-chain in `execute()`. There is no off-chain component that can whitelist a call.
2. **Reentrancy guard.** `nonReentrant` on `execute()` and `emergencyWithdraw()` prevents the classic reentrancy attack. The `ReentrancyAttacker` mock (see §17) verifies this at test time.
3. **Freeze is permanent until explicit unfreeze.** Once `policy.frozen = true` (either auto or manual), every subsequent `execute()` call reverts with `VaultFrozen()`. Only `unfreeze()` by the owner clears it.
4. **`totalSpent` is never decremented.** Only `unfreeze()` resets it to zero; this prevents an attacker from cycling spend to stay under the emergency threshold.
5. **No rescue for trapped ERC-20 tokens.** The vault handles only native token (ETH/MATIC/etc.). ERC-20 tokens sent to the vault cannot be recovered without a custom `execute()` call.
6. **Owner is immutable.** There is no `transferOwnership()` on `KeyVault`. If the owner key is compromised, the vault cannot be reassigned — only drained via `emergencyWithdraw()`.

---

## 5. VKPFactory.sol

**File:** `vaultfire-keyprotocol/contracts/VKPFactory.sol`
**Role:** VKPFactory — single-call deployer of `KeyVault` instances with per-agent discoverability.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./KeyVault.sol";
     5
     6  /// @title VKPFactory — Deploy KeyVaults with one call
     7  /// @author Vaultfire Protocol
     8  /// @notice Factory for creating KeyVault instances. Tracks all vaults created
     9  ///         per agent for discoverability.
    10  contract VKPFactory {
    11
    12      address public immutable identityRegistry;
    13      uint256 public vaultCount;
    14
    15      mapping(address => address[]) public agentVaults;
    16      address[] public allVaults;
    17
    18      event VaultDeployed(
    19          address indexed owner,
    20          address indexed vault,
    21          uint256 maxDaily,
    22          uint256 maxPerTx
    23      );
    24
    25      error ZeroAddress();
    26
    27      constructor(address _identityRegistry) {
    28          if (_identityRegistry == address(0)) revert ZeroAddress();
    29          identityRegistry = _identityRegistry;
    30      }
    31
    32      function createVault(
    33          uint256 maxPerTx,
    34          uint256 maxHourly,
    35          uint256 maxDaily,
    36          uint256 emergencyThreshold
    37      ) external returns (address vault) {
    38          KeyVault v = new KeyVault(
    39              msg.sender,
    40              identityRegistry,
    40              maxPerTx,
    41              maxHourly,
    42              maxDaily,
    43              emergencyThreshold
    44          );
    45
    46          vault = address(v);
    47          agentVaults[msg.sender].push(vault);
    48          allVaults.push(vault);
    49          vaultCount++;
    50
    51          emit VaultDeployed(msg.sender, vault, maxDaily, maxPerTx);
    52      }
    53
    54      function getAgentVaults(address agent) external view returns (address[] memory) {
    55          return agentVaults[agent];
    56      }
    57
    58      function totalVaults() external view returns (uint256) {
    59          return vaultCount;
    60      }
    61  }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 12 | `identityRegistry` is `immutable`. Factory is permanently bound to one registry — no switching. |
| 15–16 | `agentVaults` maps agent → list of vault addresses. `allVaults` is a global flat list. Both are append-only by contract logic. |
| 32–52 | **`createVault()`** — the core factory function. Deploys a new `KeyVault` via `new KeyVault(msg.sender, ...)`. The `KeyVault` constructor will itself call `identityRegistry.getAgent(msg.sender)` — so if `msg.sender` is not an ERC-8004 registered agent, the `new` call reverts and the factory transaction fails. |
| 38–44 | Parameter order passed to `KeyVault` constructor: `owner=msg.sender`, `identityRegistry`, `maxPerTx`, `maxHourly`, `maxDaily`, `emergencyThreshold`. |
| 47–49 | After successful deployment, both `agentVaults` and `allVaults` are updated, and `vaultCount` is incremented. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `address immutable` | ERC-8004 registry passed to each new `KeyVault` | Immutable |
| `vaultCount` | `uint256` | Total vaults deployed through this factory | Append-only |
| `agentVaults` | `mapping(address => address[])` | Per-agent vault address list | Append-only |
| `allVaults` | `address[]` | Global vault registry | Append-only |

### Functions

#### `createVault(uint256, uint256, uint256, uint256) → address vault`
- No access control — anyone can call, but the `KeyVault` constructor enforces ERC-8004 registration.
- Returns the deployed vault address.

#### `getAgentVaults(address agent) → address[]`
- View — returns all vaults created by a given agent.

#### `totalVaults() → uint256`
- View — global vault count.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `VaultDeployed` | Each successful `createVault()` | `owner`, `vault`, `maxDaily`, `maxPerTx` |

### Security Invariants

- The factory does not own the vaults — each `KeyVault` is owned by `msg.sender` (the agent). The factory only keeps a registry of addresses.
- An agent can deploy multiple vaults (e.g., one per task domain) — the `agentVaults` mapping supports arrays.
- No vault can be unregistered from the factory — addresses are append-only. Historical vault addresses are always discoverable.

---

## 6. SessionKeyManager.sol

**File:** `vaultfire-keyprotocol/contracts/SessionKeyManager.sol`
**Role:** SessionKeyManager — ephemeral, scoped signing keys for AI agents acting on behalf of users.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5
     6  /// @title SessionKeyManager — Time-limited, scoped signing keys for AI agents
     7  contract SessionKeyManager {
     8
     9      struct SessionKey {
    10          address owner;             // ERC-8004 registered agent
    11          address sessionAddress;    // The address authorized to sign
    12          uint256 maxValue;          // Max native token per transaction (wei)
    13          uint256 totalLimit;        // Max total spend over session lifetime (wei)
    14          uint256 totalSpent;        // Running total spent
    15          uint256 expiresAt;         // Unix timestamp when session expires
    16          uint256 createdAt;         // Unix timestamp of creation
    17          bool revoked;              // Manually revoked by owner
    18          bool allowAnyContract;     // If true, can call any contract
    19      }
    20
    21      IVaultfireIdentity public immutable identityRegistry;
    22
    23      uint256 public sessionCount;
    24
    25      mapping(uint256 => SessionKey) public sessions;
    26      mapping(uint256 => mapping(address => bool)) public sessionAllowedContracts;
    27      mapping(address => uint256[]) public ownerSessions;
    28      mapping(address => uint256) public sessionToId;    // reverse lookup
    29
    30      event SessionCreated(uint256 indexed sessionId, address indexed owner,
    31          address indexed sessionAddress, uint256 expiresAt, uint256 maxValue,
    32          uint256 totalLimit);
    33      event SessionUsed(uint256 indexed sessionId, address indexed target, uint256 value);
    34      event SessionRevoked(uint256 indexed sessionId, address indexed owner);
    35      event AllSessionsRevoked(address indexed owner);
    36
    37      error NotRegistered();
    38      error NotSessionOwner();
    39      error SessionExpired();
    40      error SessionRevoked_();
    41      error ExceedsSessionValueLimit(uint256 requested, uint256 limit);
    42      error ExceedsSessionTotalLimit(uint256 wouldSpend, uint256 limit);
    43      error ContractNotAllowedInSession(address target);
    44      error InvalidSessionAddress();
    45      error InvalidDuration();
    46      error ZeroAddress();
    47
    48      constructor(address _identityRegistry) {
    49          if (_identityRegistry == address(0)) revert ZeroAddress();
    50          identityRegistry = IVaultfireIdentity(_identityRegistry);
    51      }
    52
    53      function createSession(
    54          address sessionAddress,
    55          uint256 maxValue,
    56          uint256 totalLimit,
    57          uint256 durationSeconds,
    58          address[] calldata allowedContracts
    59      ) external returns (uint256 sessionId) {
    60          (string memory agentURI,,,) = identityRegistry.getAgent(msg.sender);
    61          if (bytes(agentURI).length == 0) revert NotRegistered();
    62          if (sessionAddress == address(0)) revert InvalidSessionAddress();
    63          if (durationSeconds == 0) revert InvalidDuration();
    64
    65          sessionId = sessionCount++;
    66          uint256 expiresAt = block.timestamp + durationSeconds;
    67
    68          sessions[sessionId] = SessionKey({
    69              owner: msg.sender,
    70              sessionAddress: sessionAddress,
    71              maxValue: maxValue,
    72              totalLimit: totalLimit,
    73              totalSpent: 0,
    74              expiresAt: expiresAt,
    70              createdAt: block.timestamp,
    71              revoked: false,
    72              allowAnyContract: allowedContracts.length == 0
    73          });
    74
    75          for (uint256 i = 0; i < allowedContracts.length; i++) {
    76              sessionAllowedContracts[sessionId][allowedContracts[i]] = true;
    77          }
    78
    79          ownerSessions[msg.sender].push(sessionId);
    80          sessionToId[sessionAddress] = sessionId;
    81
    82          emit SessionCreated(sessionId, msg.sender, sessionAddress, expiresAt, maxValue, totalLimit);
    83      }
    84
    85      function validateSession(uint256 sessionId, address target, uint256 value)
    86          external returns (bool valid) {
    87          SessionKey storage session = sessions[sessionId];
    88          if (block.timestamp > session.expiresAt) revert SessionExpired();
    89          if (session.revoked) revert SessionRevoked_();
    90          if (value > session.maxValue) revert ExceedsSessionValueLimit(value, session.maxValue);
    91          if (session.totalSpent + value > session.totalLimit)
    92              revert ExceedsSessionTotalLimit(session.totalSpent + value, session.totalLimit);
    93          if (!session.allowAnyContract && !sessionAllowedContracts[sessionId][target])
    94              revert ContractNotAllowedInSession(target);
    95          session.totalSpent += value;
    96          emit SessionUsed(sessionId, target, value);
    97          return true;
    98      }
    99
    100     function revokeSession(uint256 sessionId) external {
    101         if (sessions[sessionId].owner != msg.sender) revert NotSessionOwner();
    102         sessions[sessionId].revoked = true;
    103         emit SessionRevoked(sessionId, msg.sender);
    104     }
    105
    106     function revokeAllSessions() external {
    107         uint256[] storage ids = ownerSessions[msg.sender];
    108         for (uint256 i = 0; i < ids.length; i++) {
    109             if (!sessions[ids[i]].revoked) {
    110                 sessions[ids[i]].revoked = true;
    111                 emit SessionRevoked(ids[i], msg.sender);
    112             }
    113         }
    114         emit AllSessionsRevoked(msg.sender);
    115     }
    116
    117     function isSessionValid(uint256 sessionId) external view returns (bool) {
    118         SessionKey storage session = sessions[sessionId];
    119         return !session.revoked && block.timestamp <= session.expiresAt
    120                && session.totalSpent < session.totalLimit;
    121     }
    122
    123     function sessionRemaining(uint256 sessionId) external view returns (uint256) {
    124         SessionKey storage session = sessions[sessionId];
    125         if (session.revoked || block.timestamp > session.expiresAt) return 0;
    126         if (session.totalSpent >= session.totalLimit) return 0;
    127         return session.totalLimit - session.totalSpent;
    128     }
    129
    130     function getOwnerSessions(address owner_) external view returns (uint256[] memory) {
    131         return ownerSessions[owner_];
    132     }
    133
    134     function getSession(uint256 sessionId) external view returns (SessionKey memory) {
    135         return sessions[sessionId];
    136     }
    137
    138     function isContractAllowed(uint256 sessionId, address target) external view returns (bool) {
    139         if (sessions[sessionId].allowAnyContract) return true;
    140         return sessionAllowedContracts[sessionId][target];
    141     }
    142 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 9–19 | **`SessionKey` struct** — 9 fields. `allowAnyContract` is derived from `allowedContracts.length == 0` at creation time; it is a stored boolean, not a live computation. |
| 26 | `sessionAllowedContracts[sessionId][address]` — two-dimensional mapping for per-session contract allowlists. |
| 28 | `sessionToId[sessionAddress]` — reverse lookup from a session signing address back to its session ID. Overwrites if the same address is reused across sessions. |
| 53–83 | **`createSession()`** — checks ERC-8004 registration, allocates session ID from `sessionCount++`, computes `expiresAt = block.timestamp + durationSeconds`. If `allowedContracts` is empty, `allowAnyContract = true`. Populates allowlist mapping. |
| 65 | `sessionId = sessionCount++` — post-increment: first session has ID 0. |
| 80 | `sessionToId[sessionAddress] = sessionId` — overwrites any prior mapping for this address. If the same `sessionAddress` is reused across two sessions, only the latest session ID is in the reverse map. |
| 85–98 | **`validateSession()`** — state-mutating view: updates `totalSpent`. Callers must trust this to be called exactly once per transaction. The caller (e.g., `KeyVault`) is responsible for ensuring the session address is the actual `msg.sender` of the operation — this contract does not check `msg.sender == session.sessionAddress`. |
| 100–104 | **`revokeSession()`** — single-session revocation. Only the session owner can revoke. |
| 106–115 | **`revokeAllSessions()`** — emergency mass revocation. Iterates all session IDs for `msg.sender`. Skips already-revoked sessions. |
| 117–121 | **`isSessionValid()`** — view-only check. Note: the condition `totalSpent < totalLimit` (strict less-than) — a session where `totalSpent == totalLimit` returns `false` from this function but `validateSession()` would also revert because `totalSpent + value > totalLimit` for any positive value. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | ERC-8004 registration check | Immutable |
| `sessionCount` | `uint256` | Session ID counter (auto-increment) | Append-only |
| `sessions` | `mapping(uint256 => SessionKey)` | Session data by ID | Mutable on validate/revoke |
| `sessionAllowedContracts` | `mapping(uint256 => mapping(address => bool))` | Per-session contract allowlist | Set at creation |
| `ownerSessions` | `mapping(address => uint256[])` | Owner → session ID list | Append-only |
| `sessionToId` | `mapping(address => uint256)` | Session address → session ID reverse lookup | Overwritten on new session |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `SessionCreated` | Each `createSession()` | `sessionId`, `owner`, `sessionAddress`, `expiresAt`, `maxValue`, `totalLimit` |
| `SessionUsed` | Each successful `validateSession()` | `sessionId`, `target`, `value` |
| `SessionRevoked` | Single revocation | `sessionId`, `owner` |
| `AllSessionsRevoked` | Mass revocation | `owner` |

### Security Invariants

1. **Session keys expire automatically.** `validateSession()` checks `block.timestamp > expiresAt` and reverts. No action is required to expire a session.
2. **Total lifetime spend is a hard cap.** `totalLimit` is checked against the running `totalSpent`; no window reset resets this.
3. **Revocation is irreversible.** Once `revoked = true`, there is no `unrevokeSession()` function.
4. **`validateSession()` is not access-controlled.** Any caller can invoke it with a valid `sessionId`, `target`, and `value`. If called with `value = 0`, it would consume a transaction count and emit a `SessionUsed` event without checking the caller is actually the session key holder. The caller contract (typically `KeyVault`) must verify caller identity before calling `validateSession()`.
5. **Replay protection.** The session lifetime `totalLimit` prevents indefinite replay. The `revoked` flag enables instant revocation. Expired sessions revert on-chain.

---

## 7. ScopedDelegation.sol

**File:** `vaultfire-keyprotocol/contracts/ScopedDelegation.sol`
**Role:** ScopedDelegation — ERC-7710-compatible scoped permission delegation with progressive narrowing.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5  import "./IVaultfireBonds.sol";
     6  import "./IVaultfireReputation.sol";
     7
     8  /// @title ScopedDelegation — ERC-7710-compatible scoped permissions for AI agent delegation
     9  contract ScopedDelegation {
    10
    11     struct Scope {
    12         uint256 maxValuePerTx;
    13         uint256 maxTotalValue;
    14         uint256 totalSpent;
    15         uint256 maxTxCount;          // 0 = unlimited
    16         uint256 txCount;
    17         uint256 rateLimit;           // Min seconds between transactions (0 = no limit)
    18         uint256 lastTxAt;
    19         uint256 expiresAt;
    20         uint256 createdAt;
    21         address grantor;
    22         address grantee;
    23         bool revoked;
    24         bool active;
    25     }
    26
    27     struct DelegationChain {
    28         uint256[] scopeIds;
    28         uint256 depth;
    29         address rootGrantor;
    30         bool valid;
    31     }
    32
    33     IVaultfireIdentity public immutable identityRegistry;
    34     IVaultfireBonds public immutable bondsRegistry;
    35     IVaultfireReputation public immutable reputationRegistry;
    36
    37     address public owner;
    38
    38     uint256 public totalScopes;
    39     uint256 public totalChains;
    40     uint256 public constant MAX_CHAIN_DEPTH = 5;
    41
    42     mapping(uint256 => Scope) public scopes;
    43     mapping(uint256 => DelegationChain) public chains;
    44     mapping(address => uint256[]) public grantedScopes;
    45     mapping(address => uint256[]) public receivedScopes;
    46     mapping(uint256 => mapping(address => bool)) public contractAllowlist;
    47     mapping(uint256 => mapping(bytes4 => bool)) public functionAllowlist;
    48     mapping(uint256 => bool) public hasContractRestrictions;
    49     mapping(uint256 => bool) public hasFunctionRestrictions;
    50
    51     event ScopeCreated(uint256 indexed scopeId, address indexed grantor,
    52         address indexed grantee, uint256 expiresAt);
    53     event ScopeRevoked(uint256 indexed scopeId, address indexed revokedBy);
    54     event ScopeUsed(uint256 indexed scopeId, address indexed grantee, uint256 value);
    55     event ChainCreated(uint256 indexed chainId, address indexed rootGrantor, uint256 depth);
    56     event ContractAllowlistUpdated(uint256 indexed scopeId, address indexed contractAddr, bool allowed);
    57     event FunctionAllowlistUpdated(uint256 indexed scopeId, bytes4 indexed selector, bool allowed);
    58
    59     modifier onlyOwner() { ... }
    60
    60     modifier onlyRegisteredAgent(address agent) {
    61         (string memory uri, bool active, , ) = identityRegistry.getAgent(agent);
    62         require(bytes(uri).length > 0 && active, "...");
    63         _;
    64     }
    65
    65     constructor(address _identityRegistry, address _bondsRegistry, address _reputationRegistry) { ... }
    66
    67     function createScope(address grantee, uint256 maxValuePerTx, uint256 maxTotalValue,
    68         uint256 maxTxCount, uint256 rateLimit, uint256 duration)
    69         external onlyRegisteredAgent(msg.sender) onlyRegisteredAgent(grantee)
    70         returns (uint256 scopeId) { ... }
    71
    72     function subDelegate(uint256 parentScopeId, address grantee, uint256 maxValuePerTx,
    73         uint256 maxTotalValue, uint256 maxTxCount, uint256 rateLimit, uint256 duration)
    74         external onlyRegisteredAgent(grantee)
    75         returns (uint256 scopeId) { ... }
    76
    77     function setContractAllowlist(uint256 scopeId, address contractAddr, bool allowed) external { ... }
    78     function setFunctionAllowlist(uint256 scopeId, bytes4 selector, bool allowed) external { ... }
    79
    80     function useScope(uint256 scopeId, uint256 value, address targetContract,
    81         bytes4 functionSelector) external returns (bool) { ... }
    82
    82     function revokeScope(uint256 scopeId) external { ... }
    83
    84     // view functions
    84     function isScopeValid(uint256 scopeId) external view returns (bool) { ... }
    85     function getScopeRemaining(uint256 scopeId) external view returns (...) { ... }
    86     function getGrantedScopes(address agent) external view returns (uint256[] memory) { ... }
    87     function getReceivedScopes(address agent) external view returns (uint256[] memory) { ... }
    88     function transferOwnership(address newOwner) external onlyOwner { ... }
    89 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 11–25 | **`Scope` struct** — 14 fields. `rateLimit` is the minimum seconds between consecutive transactions; `lastTxAt` tracks the last transaction timestamp for rate-limit enforcement. `active` and `revoked` are separate flags: `active = false && revoked = false` would represent a scope that was never activated, but in practice all newly created scopes start as `active = true, revoked = false`. |
| 27–31 | **`DelegationChain` struct** — contains an array of scope IDs from root to leaf. The `chains` mapping is defined but no creation function is implemented in this contract (the `ChainCreated` event exists but is never emitted). This struct is infrastructure for future chain tracking. |
| 40 | `MAX_CHAIN_DEPTH = 5` — constant preventing unbounded delegation chains. Not enforced in `subDelegate()` directly (depth tracking would require walking the chain), but signals design intent. |
| 46–49 | Per-scope allowlist mappings. `hasContractRestrictions[scopeId]` is a fast gate: if false, no contract check runs. Same pattern for functions. |
| 60–64 | `onlyRegisteredAgent(agent)` modifier checks both `agentURI.length > 0` **and** `active == true` — stricter than `KeyVault` which only checks URI length. |
| 67–70 | **`createScope()`** — both `msg.sender` (grantor) and `grantee` must be registered active agents. Cannot self-delegate. Duration bounded to `[0, 365 days]`. Must have at least `maxValuePerTx > 0` or `maxTxCount > 0`. |
| 131–166 | Full `createScope()` implementation: allocates `scopeId = totalScopes++`, populates scope struct with `expiresAt = block.timestamp + duration`, pushes to both `grantedScopes[grantor]` and `receivedScopes[grantee]`. |
| 177–227 | **`subDelegate()`** — the progressive narrowing enforcement engine. The caller must be the `grantee` of the parent scope. Five narrowing checks: (1) `maxValuePerTx <= parent.maxValuePerTx`, (2) `maxTotalValue <= parentRemaining` (remaining = `maxTotalValue - totalSpent`), (3) if parent has a tx count limit, new `maxTxCount <= parentRemainingTx`, (4) `rateLimit >= parent.rateLimit` (more restrictive = larger minimum gap), (5) `duration <= parentRemainingDuration`. Any violation reverts. |
| 192–202 | The narrowing invariants are strict checks — `require(maxValuePerTx <= parent.maxValuePerTx, ...)`. Note this allows equality (sub-delegation at the exact same limit as the parent), not just strictly less. |
| 234–243 | **`setContractAllowlist()`** — grantor-only. Sets `hasContractRestrictions[scopeId] = true` when any entry is added. There is no mechanism to unset `hasContractRestrictions` even if all entries are removed — once restrictions are added, they remain checked. |
| 266–308 | **`useScope()`** — stateful validation. Only callable by the scope's `grantee`. Checks: active+not-revoked, not expired, value limit, total value limit, tx count (if set), rate limit, contract allowlist (if set), function allowlist (if set). Then updates `totalSpent`, `txCount`, `lastTxAt`. |
| 315–323 | **`revokeScope()`** — sets both `revoked = true` and `active = false`. Either flag alone would block `useScope()`; setting both is redundant but unambiguous. Can be called by grantor or contract owner. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration check | Immutable |
| `bondsRegistry` | `IVaultfireBonds immutable` | Bond check (present in constructor, not used in scope logic directly) | Immutable |
| `reputationRegistry` | `IVaultfireReputation immutable` | Reputation check (present, not used in scope logic directly) | Immutable |
| `owner` | `address` | Contract owner — can revoke any scope | Transferable |
| `totalScopes` | `uint256` | Scope ID counter | Monotone increasing |
| `scopes` | `mapping(uint256 => Scope)` | All scopes by ID | Mutable |
| `contractAllowlist` | `mapping(uint256 => mapping(address => bool))` | Per-scope contract allowlist | Mutable (grantor) |
| `functionAllowlist` | `mapping(uint256 => mapping(bytes4 => bool))` | Per-scope function selector allowlist | Mutable (grantor) |
| `hasContractRestrictions` | `mapping(uint256 => bool)` | Fast gate for contract checks | Set on first addition, never cleared |
| `hasFunctionRestrictions` | `mapping(uint256 => bool)` | Fast gate for function selector checks | Set on first addition, never cleared |

### Math: Scoped Delegation Expiry

For a root scope created at time T₀ with duration D:

\[ expiresAt = T_0 + D \]

For a sub-delegation from that scope created at time T₁ with requested duration d:

\[ \text{allowed} \iff d \leq expiresAt - T_1 = (T_0 + D) - T_1 \]

Example: Root scope created at T₀=0 with D=30 days (expiresAt=30d). Sub-delegation attempted at T₁=20 days: maximum allowed duration = 30d − 20d = 10 days.

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `ScopeCreated` | `createScope()` and `subDelegate()` | `scopeId`, `grantor`, `grantee`, `expiresAt` |
| `ScopeRevoked` | `revokeScope()` | `scopeId`, `revokedBy` |
| `ScopeUsed` | `useScope()` | `scopeId`, `grantee`, `value` |
| `ChainCreated` | (never emitted — infrastructure) | `chainId`, `rootGrantor`, `depth` |
| `ContractAllowlistUpdated` | `setContractAllowlist()` | `scopeId`, `contractAddr`, `allowed` |
| `FunctionAllowlistUpdated` | `setFunctionAllowlist()` | `scopeId`, `selector`, `allowed` |

### Security Invariants

1. **Progressive narrowing is enforced at `subDelegate()` time.** Sub-scopes can never exceed parent limits on value, tx count, or duration. Rate limits can only increase.
2. **Grantee cannot self-delegate.** `require(grantee != msg.sender)`.
3. **Scope usage is caller-gated.** `useScope()` requires `scope.grantee == msg.sender`. An address that is not the grantee cannot record usage against a scope.
4. **Rate limiting prevents rapid-fire abuse.** `rateLimit` sets minimum time between transactions within a scope.
5. **Contract and function restriction flags are sticky.** Once `hasContractRestrictions` is set, it cannot be unset — this prevents a grantor from silently removing restrictions without revoking the scope.

---

## 8. DelegationRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/DelegationRegistry.sol`
**Role:** DelegationRegistry — encrypted key delegation between verified AI agents with live trust reverification.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "./IVaultfireIdentity.sol";
     5  import "./IVaultfireBonds.sol";
     6  import "./IVaultfireReputation.sol";
     7
     8  /// @title DelegationRegistry — Encrypted key delegation between verified AI agents
     9  contract DelegationRegistry {
    10
    11     struct Delegation {
    12         address delegator;
    13         address delegate;
    14         uint256 maxValue;
    15         uint256 totalLimit;
    16         uint256 totalSpent;
    17         uint256 expiresAt;
    18         uint256 createdAt;
    19         uint256 minBondRequired;
    20         uint256 minReputation;
    21         bool revoked;
    22         bool accepted;
    23         bool hasContractAllowlist;
    24         bool hasFunctionAllowlist;
    25         bytes32 encryptedKeyHash;    // Off-chain encrypted session key reference
    26     }
    27
    28     IVaultfireIdentity public immutable identityRegistry;
    29     IVaultfireBonds public immutable bondsRegistry;
    30     IVaultfireReputation public immutable reputationRegistry;
    31
    32     uint256 public delegationCount;
    33
    34     mapping(uint256 => Delegation) public delegations;
    35     mapping(uint256 => mapping(address => bool)) public delegationAllowedContracts;
    36     mapping(uint256 => mapping(bytes4 => bool)) public delegationAllowedFunctions;
    37     mapping(address => uint256[]) public delegatorDelegations;
    38     mapping(address => uint256[]) public delegateDelegations;
    39     mapping(uint256 => uint256) public delegationBondIds;    // bondId used at acceptance
    40
    41     event DelegationCreated(...);
    42     event DelegationAccepted(uint256 indexed delegationId, address indexed delegate);
    43     event DelegationUsed(uint256 indexed delegationId, address indexed target, uint256 value);
    44     event DelegationRevoked(uint256 indexed delegationId, address indexed delegator);
    45     event AllDelegationsRevoked(address indexed delegator);
    46     event TrustViolation(uint256 indexed delegationId, string reason);
    47
    48     // ... errors ...
    49
    50     constructor(address _identityRegistry, address _bondsRegistry, address _reputationRegistry) { ... }
    51
    52     function createDelegation(address delegate, uint256 maxValue, uint256 totalLimit,
    53         uint256 durationSeconds, uint256 minBondRequired, uint256 minReputation,
    54         address[] calldata allowedContracts, bytes4[] calldata allowedFunctions,
    55         bytes32 encryptedKeyHash) external returns (uint256 delegationId) { ... }
    56
    57     function acceptDelegation(uint256 delegationId, uint256 bondId) external { ... }
    58
    58     function verifyTrust(uint256 delegationId) external returns (bool trusted) { ... }
    59
    59     function validateDelegation(uint256 delegationId, address target, uint256 value,
    60         bytes4 selector) external returns (bool valid) { ... }
    61
    62     function revokeDelegation(uint256 delegationId) external { ... }
    63     function revokeAllDelegations() external { ... }
    64
    65     // view functions + _verifyTrustRequirements internal
    66 }
```

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 11–26 | **`Delegation` struct** — 14 fields. `encryptedKeyHash` is a `bytes32` reference to an off-chain XMTP/IPFS encrypted session key. This is the "XMTP for keys" aspect: the actual key material is off-chain encrypted; the hash is the on-chain anchor. |
| 19–20 | `minBondRequired` and `minReputation` — the trust gates. Both can be zero (open delegation) or non-zero (trust-gated). |
| 21–22 | `revoked` and `accepted` are separate flags. A delegation that has never been accepted is not usable (`validateDelegation()` reverts with `DelegationNotAccepted`). |
| 34 | `delegationBondIds[delegationId]` — stores the `bondId` the delegate presented at acceptance for future re-verification. |
| 140–202 | **`createDelegation()`** — both `msg.sender` (delegator) and `delegate` must be ERC-8004 registered. Cannot self-delegate. `durationSeconds == 0` reverts. Sets `accepted = false`; the delegation is pending until `acceptDelegation()` is called. |
| 211–241 | **`acceptDelegation(uint256 delegationId, uint256 bondId)`** — delegate calls this to accept the delegation. Validates: not already accepted, not revoked, not expired. If `minBondRequired > 0`: calls `bondsRegistry.getBond(bondId)`, verifies `creator == msg.sender && active && stakeAmount >= minBondRequired`. Stores `bondId` in `delegationBondIds` for future reverification. If `minReputation > 0`: calls `reputationRegistry.getReputation(msg.sender)`, verifies `score >= minReputation`. |
| 253–261 | **`verifyTrust(uint256 delegationId)`** — permissionless re-verification. Returns `false` immediately if already revoked, not accepted, or expired. Otherwise calls `_verifyTrustRequirements()` which may auto-revoke. Returns `!d.revoked` after the check. |
| 273–315 | **`validateDelegation()`** — the execution gate. Checks revoked, accepted, expiry, per-tx value, total limit, contract allowlist, function selector allowlist. Updates `totalSpent`. |
| 322–338 | **`revokeDelegation()`** and **`revokeAllDelegations()`** — immediate revocation by delegator. Mass revocation iterates the full `delegatorDelegations[msg.sender]` array. |
| 385–423 | **`_verifyTrustRequirements()`** — **critical design decision**: this function does NOT revert on trust failure. It calls `bondsRegistry.getBond(...)` and `reputationRegistry.getReputation(...)` in `try/catch` blocks. If the bond or reputation check fails, it sets `d.revoked = true` and returns. If it reverted here, the EVM would roll back the state change and the delegation would remain active — a fail-open security hole. By not reverting, the revocation is committed to chain state even on failure. |
| 391–404 | Bond check in `_verifyTrustRequirements()`: `try bondsRegistry.getBond(bondId) returns (address creator, uint256 stakeAmount, uint256, bool active)`. Verifies `creator == d.delegate && active && stakeAmount >= d.minBondRequired`. If any condition fails, or if the call reverts (the `catch` block), the delegation is auto-revoked with a `TrustViolation` event. |
| 408–422 | Reputation check: `try reputationRegistry.getReputation(d.delegate) returns (uint256 score, ...)`. If `score < d.minReputation`, auto-revoke. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration gate | Immutable |
| `bondsRegistry` | `IVaultfireBonds immutable` | Bond verification | Immutable |
| `reputationRegistry` | `IVaultfireReputation immutable` | Reputation verification | Immutable |
| `delegationCount` | `uint256` | Delegation ID counter | Monotone increasing |
| `delegations` | `mapping(uint256 => Delegation)` | All delegations by ID | Mutable |
| `delegationAllowedContracts` | `mapping(uint256 => mapping(address => bool))` | Per-delegation contract allowlist | Set at creation |
| `delegationAllowedFunctions` | `mapping(uint256 => mapping(bytes4 => bool))` | Per-delegation function selector allowlist | Set at creation |
| `delegatorDelegations` | `mapping(address => uint256[])` | Delegator → delegation ID list | Append-only |
| `delegateDelegations` | `mapping(address => uint256[])` | Delegate → delegation ID list | Append-only |
| `delegationBondIds` | `mapping(uint256 => uint256)` | Delegation → bond ID used at acceptance | Set on acceptance |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `DelegationCreated` | `createDelegation()` | `delegationId`, `delegator`, `delegate`, `expiresAt`, `maxValue`, `totalLimit`, `minBondRequired`, `minReputation` |
| `DelegationAccepted` | `acceptDelegation()` | `delegationId`, `delegate` |
| `DelegationUsed` | `validateDelegation()` | `delegationId`, `target`, `value` |
| `DelegationRevoked` | Manual revocation | `delegationId`, `delegator` |
| `AllDelegationsRevoked` | Mass revocation | `delegator` |
| `TrustViolation` | Auto-revoke in `_verifyTrustRequirements()` | `delegationId`, `reason` string |

### Security Invariants

1. **Two-phase trust gate.** Delegation is created by delegator (pending) and only becomes active when delegate explicitly accepts and proves they meet bond + reputation requirements.
2. **Auto-revocation is fail-closed.** `_verifyTrustRequirements()` uses `try/catch` + commit pattern: any external call failure revokes the delegation rather than keeping it active.
3. **Anyone can trigger reverification.** `verifyTrust()` is permissionless. Watchdog bots can monitor delegation status and call `verifyTrust()` whenever bond or reputation changes.
4. **No delegation rescue.** Once revoked (manually or automatically), a delegation cannot be reinstated. A new delegation must be created and accepted.
5. **`encryptedKeyHash` is a reference, not the key.** Actual key material lives off-chain. The hash provides auditability without key exposure on a public ledger.

---

## 9. AgentSLAEnforcer.sol

**File:** `vaultfire-keyprotocol/contracts/AgentSLAEnforcer.sol`
**Role:** AgentSLAEnforcer — on-chain SLA commitment registry with staked collateral and automatic penalty slashing.

### Full Source with Line Numbers

(Full source: 372 lines — see source files. Key structures documented below.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 19–31 | **`SLACommitment` struct** — three metrics (`uptimeTarget`, `responseTimeMs`, `accuracyTarget`) expressed in basis points. `stakedAmount` is native token locked as SLA guarantee. `penaltyRate` is in basis points of remaining stake per violation. |
| 33–46 | **`SLAReport` struct** — captures a measurement period with individual violation flags per metric. `penaltyApplied` records how much was actually slashed. |
| 63–69 | **Constants**: `MIN_UPTIME_TARGET = 5000` (50% minimum — no SLA below 50% uptime), `MAX_RESPONSE_TIME = 60000` (60 seconds max), `MIN_ACCURACY_TARGET = 5000`, duration bounded to `[7 days, 365 days]`, `MAX_PENALTY_RATE = 1000` (10% maximum per-violation slash), `MIN_REPORT_PERIOD = 1 hour`. |
| 76–77 | `monitors` mapping — authorized addresses that can submit SLA performance reports. The owner is always an authorized monitor. |
| 103–106 | `onlyMonitor` modifier — owner OR any authorized monitor address. |
| 143–177 | **`createSLA()`** — `payable`, requires `msg.value > 0`. All three metrics validated against their constants. SLA stored with `activeSince = block.timestamp` and `expiresAt = block.timestamp + duration`. |
| 190–276 | **`submitReport()`** — monitor-only. Validates: SLA active, not expired, period valid (past, at least 1 hour long). Detects up to 3 violations per report (uptime, response time, accuracy). Penalty calculation detailed below. |
| 207–211 | Violation detection: `uptimeViolation = measuredUptime < sla.uptimeTarget`, `responseViolation = measuredResponseTime > sla.responseTimeMs`, `accuracyViolation = measuredAccuracy < sla.accuracyTarget`. |
| 214–252 | Penalty block: counts violations, computes penalty, updates counters, deactivates SLA if stake fully consumed. |
| 283–299 | **`expireSLA()`** — called after `block.timestamp >= sla.expiresAt`. Sets `active = false`, returns `remainingStake = stakedAmount - totalPenalties` to agent. |

### Math: SLA Penalty

For a report with `n` violations:

\[ \text{remainingStake} = \text{stakedAmount} - \text{totalPenalties} \]

\[ \text{penalty} = \left\lfloor \frac{\text{remainingStake} \times \text{penaltyRate} \times n}{10000} \right\rfloor \]

where `penaltyRate` is in basis points.

**Penalty is capped at `remainingStake`** — the agent's stake can reach zero but cannot go negative.

Each violation type (uptime, response, accuracy) receives `penalty / n` in the emitted event.

**Example:**
- `stakedAmount = 1 ETH`, `totalPenalties = 0.1 ETH`, `penaltyRate = 500` (5%), `n = 2` violations
- `remainingStake = 0.9 ETH`
- `penalty = (0.9e18 × 500 × 2) / 10000 = 0.09 ETH`
- Each of the two `SLAViolation` events logs `0.045 ETH`
- After report: `totalPenalties = 0.19 ETH`, `penaltyPool += 0.09 ETH`

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin (monitor management, penalty withdrawal) | Transferable |
| `totalSLAs / totalReports / totalViolations / totalPenaltiesCollected` | `uint256` | Global counters | Monotone increasing |
| `slas` | `mapping(uint256 => SLACommitment)` | SLA data by ID | Mutable |
| `reports` | `mapping(uint256 => SLAReport)` | Report data by ID | Append-only |
| `monitors` | `mapping(address => bool)` | Authorized report submitters | Mutable (owner) |
| `penaltyPool` | `uint256` | Accumulated penalty balance | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `SLACreated` | `createSLA()` | `slaId`, `agent`, `uptimeTarget`, `responseTimeMs`, `accuracyTarget`, `stake` |
| `SLAExpired` | `expireSLA()` or stake exhaustion | `slaId`, `agent`, `totalViolations` |
| `SLAReportSubmitted` | `submitReport()` | `reportId`, `slaId`, `reporter`, `hasViolation` |
| `SLAViolation` | Per-violation in `submitReport()` | `slaId`, `agent`, `penalty` (per-violation share), `violationType` |
| `SLAStakeReturned` | `expireSLA()` with remaining stake | `slaId`, `agent`, `amount` |
| `MonitorUpdated` | `setMonitor()` | `monitor`, `authorized` |
| `PenaltyWithdrawn` | `withdrawPenalties()` | `to`, `amount` |

### Security Invariants

1. **Staking is required.** `msg.value > 0` enforced at `createSLA()`. An SLA without stake is not permitted.
2. **Penalty cannot exceed remaining stake.** Cap at `remainingStake` prevents arithmetic underflow.
3. **Report period must be in the past.** `require(periodEnd <= block.timestamp)` prevents forward-dated reports.
4. **Monitor access is strictly controlled.** Only owner can add/remove monitors. A compromised monitor can submit false reports — monitor addresses should be hardware-attested or multi-sig controlled.
5. **SLA stake is not slashable externally.** Only `submitReport()` can change `totalPenalties`; only `expireSLA()` returns the remainder. There is no emergency drain function for SLA stakes.

---

## 10. AgentInsurancePool.sol

**File:** `vaultfire-keyprotocol/contracts/AgentInsurancePool.sol`
**Role:** AgentInsurancePool — pooled insurance for AI agents with reputation-tiered premiums and arbiter-resolved claims.

### Full Source with Line Numbers

(Full source: 320 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 19–28 | **`Policy` struct** — `riskTier` is `uint256` (1, 2, or 3). `claimsPaid` accumulates total claims paid against this policy to track per-policy coverage exhaustion. |
| 30–39 | **`Claim` struct** — `evidenceURI` points to external evidence (IPFS). `resolvedAt = 0` when pending. `ClaimStatus` enum is `{Pending, Approved, Denied, Paid}` — note `Approved` and `Paid` are separate states (arbiter approves, then payout executes). |
| 41 | `ClaimStatus { Pending, Approved, Denied, Paid }` — `resolveClaim()` transitions from `Pending` directly to `Paid` (if approved) or `Denied` in a single transaction, skipping `Approved` as an intermediate state. The `Approved` enum value exists for potential future multi-step resolution. |
| 59–62 | **Premium rate constants**: `LOW_RISK_RATE = 100` (1%), `MEDIUM_RISK_RATE = 250` (2.5%), `HIGH_RISK_RATE = 500` (5%). These are basis points of coverage. |
| 64–67 | **Risk tier thresholds**: reputation ≥ 70 → tier 1 (low), ≥ 40 → tier 2 (medium), below 40 → tier 3 (high). |
| 136–169 | **`createPolicy()`** — `onlyRegisteredAgent(msg.sender)`, `payable`. Enforces one active policy per agent (`hasActivePolicy`). Calls `_calculateRiskTier()` which queries the reputation registry. Calls `_calculatePremium()` to get the required premium. If `msg.value < requiredPremium`, reverts. Adds `msg.value` to `totalPoolBalance`. |
| 173–182 | **`expirePolicy()`** — permissionless. Moves policy to inactive and clears `hasActivePolicy`. Does not refund any premium. |
| 192–219 | **`fileClaim()`** — permissionless. Any address can file a claim against an active, non-expired policy. Validates amount ≤ `coverageLimit` and cumulative `claimsPaid + amount ≤ coverageLimit`. |
| 224–249 | **`resolveClaim()`** — arbiter-only. If approved: checks `totalPoolBalance >= amount`, updates `policy.claimsPaid`, decrements `totalPoolBalance`, increments `totalClaimsPaid`, sends ETH to claimant. If denied: sets status to `Denied`. No refund of premium either way. |
| 256–261 | **`_calculateRiskTier()`** — pure reputation read; does not consider bond size or violation history. |
| 264–276 | **`_calculatePremium()`** — `pure` function. Formula below. |

### Math: Insurance Pool

**Risk Tier Assignment:**

\[ \text{tier} = \begin{cases} 1 & \text{if } \text{rep} \geq 70 \\ 2 & \text{if } 40 \leq \text{rep} < 70 \\ 3 & \text{if } \text{rep} < 40 \end{cases} \]

**Premium Calculation:**

\[ \text{premium} = \frac{\text{coverageLimit} \times \text{rate} \times \text{duration}}{365\,\text{days} \times 10000} \]

where `rate` ∈ {100, 250, 500} basis points.

**Examples (1 ETH coverage, 30-day policy):**

| Tier | Rate | Premium |
|------|------|---------|
| 1 (low risk, rep ≥ 70) | 100 bp (1%) | `1e18 × 100 × 30d / (365d × 10000)` ≈ 0.000822 ETH |
| 2 (medium risk, rep 40–69) | 250 bp (2.5%) | ≈ 0.00205 ETH |
| 3 (high risk, rep < 40) | 500 bp (5%) | ≈ 0.00411 ETH |

**Coverage Exhaustion:**

`policy.claimsPaid + newClaim.amount ≤ policy.coverageLimit` — once the coverage limit is fully consumed, no further claims can be filed against that policy.

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin (set arbiter, transfer ownership) | Transferable |
| `arbiter` | `address` | Claim resolver — initially set to `owner` | Mutable (owner) |
| `totalPoolBalance` | `uint256` | Aggregate premium balance available for claims | Mutable |
| `totalPolicies / totalClaims / totalClaimsPaid` | `uint256` | Global statistics | Monotone increasing |
| `policies` | `mapping(uint256 => Policy)` | Policy data by ID | Mutable |
| `claims` | `mapping(uint256 => Claim)` | Claim data by ID | Mutable |
| `hasActivePolicy` | `mapping(address => bool)` | One-policy-per-agent enforcement | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `PolicyCreated` | `createPolicy()` | `policyId`, `agent`, `coverageLimit`, `premium`, `riskTier` |
| `PolicyExpired` | `expirePolicy()` | `policyId`, `agent` |
| `ClaimFiled` | `fileClaim()` | `claimId`, `policyId`, `claimant`, `amount` |
| `ClaimResolved` | `resolveClaim()` | `claimId`, `status`, `payout` |
| `PremiumDeposited` | `createPolicy()` | `agent`, `amount` |
| `ArbiterUpdated` | `setArbiter()` | `oldArbiter`, `newArbiter` |

### Security Invariants

1. **Single active policy per agent.** `hasActivePolicy[agent]` prevents double-coverage abuse.
2. **Arbiter is the single claim resolution authority.** A compromised arbiter can approve fraudulent claims. Multi-sig arbiter address is strongly recommended.
3. **No pool solvency guarantee.** `totalPoolBalance` can theoretically be drained if many large claims are approved simultaneously. No partial-payout mechanism exists.
4. **Premium is locked at policy creation time.** Risk tier is computed once from the reputation registry at `createPolicy()` time, not dynamically repriced. An agent's reputation can drop after purchasing insurance without penalty repricing.
5. **Evidence URI is not validated on-chain.** `evidenceURI` is a string; empty-string check (`bytes(evidenceURI).length > 0`) is the only validation.

---

## 11. AgentSafetyManager.sol

**File:** `vaultfire-keyprotocol/contracts/AgentSafetyManager.sol`
**Role:** AgentSafetyManager — kill switch, safe mode, and EU AI Act Article 36 compliant safety controls.

### Full Source with Line Numbers

(Full source: 320 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 20 | `SafetyStatus { Active, SafeMode, Frozen }` — the three safety states. `Active = 0`, `SafeMode = 1`, `Frozen = 2` in EVM encoding. |
| 22–30 | **`AgentSafetyState` struct** — records the safety state, who triggered it, human-readable reason, and counts of auto vs. manual trigger events. `exists` flag distinguishes "never had a record" (implicitly `Active`) from "explicitly set to Active". |
| 32–40 | **`SafetyEvent` struct** — full audit trail entry with `fromStatus` and `toStatus`. Pushed to `safetyLog[]` on every status change. |
| 53–54 | `safeModeThreshold = 30` and `freezeThreshold = 10` — default thresholds. Configurable by owner, with the invariant `safeModeThreshold > freezeThreshold`. |
| 83–100 | `onlyOwner` and `onlySafetyOperator` modifiers. Safety operators can trigger safety actions but cannot update thresholds or add other operators. |
| 125–133 | **`freezeAgent()`** — kill switch. Callable by owner or any safety operator. Requires agent to be registered. Calls `_updateSafetyStatus()` with `isAutoTrigger = false`. |
| 138–143 | **`activateSafeMode()`** — restricts agent to read-only mode. Same permissions as `freezeAgent()`. |
| 148–157 | **`reactivateAgent()`** — unfreezes or removes safe mode. Requires an existing safety record; cannot "activate" an agent that was never recorded (they are implicitly active). |
| 162–176 | **`checkAndEnforceSafety()`** — permissionless reputation check. Anyone can call. Reads score from `reputationRegistry`. If `score <= freezeThreshold (10)`: auto-freeze (if not already frozen). If `score <= safeModeThreshold (30)`: auto-safe-mode (if currently Active). Note: if agent is already in SafeMode and score drops to freezeThreshold, a second call is needed (the first call puts them in SafeMode; a second call would freeze them since the SafeMode branch is `state.status == SafetyStatus.Active`). |
| 182–235 | **`_updateSafetyStatus()`** — internal. Updates `agentSafety[agent]`, adjusts global counters (`totalFrozenAgents`, `totalSafeModeAgents`), appends `SafetyEvent` to `safetyLog` and `agentSafetyEvents[agent]`. Emits the typed event. |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / bondsRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin | Transferable |
| `safeModeThreshold` | `uint256` | Rep below this → auto safe mode (default 30) | Mutable (owner) |
| `freezeThreshold` | `uint256` | Rep below this → auto freeze (default 10) | Mutable (owner) |
| `agentSafety` | `mapping(address => AgentSafetyState)` | Per-agent safety state | Mutable |
| `safetyLog` | `SafetyEvent[]` | Global audit trail | Append-only |
| `agentSafetyEvents` | `mapping(address => uint256[])` | Agent → indices into `safetyLog` | Append-only |
| `safetyOperators` | `mapping(address => bool)` | Authorized safety triggerers | Mutable (owner) |
| `totalFrozenAgents / totalSafeModeAgents` | `uint256` | Global safety counters | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `AgentFrozen` | Freeze via operator or auto | `agent`, `triggeredBy`, `reason` |
| `AgentSafeModeActivated` | Safe mode via operator or auto | `agent`, `triggeredBy`, `reason` |
| `AgentReactivated` | Reactivation | `agent`, `triggeredBy`, `reason` |
| `SafetyThresholdsUpdated` | `setThresholds()` | `safeModeThreshold`, `freezeThreshold` |
| `SafetyOperatorUpdated` | `setSafetyOperator()` | `operator`, `authorized` |

### Security Invariants

1. **Permissionless auto-trigger.** Anyone can call `checkAndEnforceSafety()`. This allows watchdog bots and users to enforce safety without relying on the protocol team.
2. **Safety state changes cannot be triggered by unregistered addresses.** The `onlyRegisteredAgent` modifier guards all safety-setting functions, preventing spurious records for non-existent agents.
3. **Audit trail is append-only.** The `safetyLog` array never deletes or modifies entries. Regulatory auditors can reconstruct every status transition with full context.
4. **Threshold invariant.** `setThresholds()` enforces `safeModeThreshold > freezeThreshold` — the two thresholds cannot overlap or invert.

---

## 12. ComplianceRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/ComplianceRegistry.sol`
**Role:** ComplianceRegistry — EU AI Act on-chain attestation system with authorized auditor management.

### Full Source with Line Numbers

(Full source: 298 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 17 | `RiskLevel { Minimal, Limited, High, Unacceptable }` — EU AI Act Article 5/6/9 risk tiers. |
| 19 | `ComplianceStatus { Pending, Compliant, NonCompliant, Expired, Revoked }` — `Pending` exists as a default enum value but no attestation is ever created with `Pending` status; all issued attestations start as `Compliant`. |
| 21–35 | **`ComplianceAttestation` struct** — 14 fields. `humanOversightVerified`, `technicalDocVerified`, `postMarketMonitoring`, `incidentReporting` map directly to EU AI Act high-risk requirements. |
| 37–42 | **`AuditorInfo` struct** — tracks authorization timestamp and count of attestations issued. `auditorURI` is a link to auditor credentials (e.g., accreditation certificate on IPFS). |
| 56–57 | Constants: `MIN_ATTESTATION_DURATION = 30 days`, `MAX_ATTESTATION_DURATION = 730 days` (2 years). |
| 62 | `isCompliant[agent]` — fast O(1) compliance lookup. This mapping is updated by `_recalculateCompliance()` when attestations expire or are revoked. |
| 119–173 | **`issueAttestation()`** — authorized auditor only. Hard block: `Unacceptable` risk level cannot be attested. For `High` risk: all four boolean flags must be `true`. Creates attestation with `status = Compliant`, updates `isCompliant[agent] = true` if not already. |
| 178–190 | **`revokeAttestation()`** — authorized auditor or owner. Sets `status = Revoked`, records `revokedAt` timestamp. Calls `_recalculateCompliance()` to check if agent still has any valid attestation. |
| 194–203 | **`expireAttestation()`** — permissionless. Any caller can mark an attestation expired once `block.timestamp >= att.expiresAt`. Calls `_recalculateCompliance()`. |
| 210–227 | **`_recalculateCompliance()`** — loops over all attestations for the agent, looking for any with `status == Compliant && block.timestamp < expiresAt`. If none found, sets `isCompliant[agent] = false` and decrements `totalCompliantAgents`. |
| 234–248 | **`checkCompliance()`** — view function computing current compliance from all attestations (does not use the `isCompliant` cache, so returns the live computed value). |

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry` | `IVaultfireIdentity immutable` | Agent registration check | Immutable |
| `owner` | `address` | Admin — auditor management | Transferable |
| `totalAttestations / totalAuditors / totalCompliantAgents` | `uint256` | Global statistics | Mutable |
| `attestations` | `mapping(uint256 => ComplianceAttestation)` | Attestation data | Mutable |
| `agentAttestations` | `mapping(address => uint256[])` | Agent → attestation IDs | Append-only |
| `auditors` | `mapping(address => AuditorInfo)` | Authorized auditor registry | Mutable (owner) |
| `isCompliant` | `mapping(address => bool)` | Fast compliance lookup | Mutable |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `AttestationIssued` | `issueAttestation()` | `attestationId`, `agent`, `auditor`, `riskLevel`, `framework` |
| `AttestationRevoked` | `revokeAttestation()` | `attestationId`, `agent`, `auditor`, `reason` |
| `AttestationExpired` | `expireAttestation()` | `attestationId`, `agent` |
| `AuditorAuthorized` | `authorizeAuditor()` | `auditor`, `auditorURI` |
| `AuditorRevoked` | `revokeAuditor()` | `auditor` |
| `ComplianceStatusChanged` | Compliance gained or lost | `agent`, `isCompliant` |

### Security Invariants

1. **`Unacceptable` risk agents cannot receive attestations.** Hard-coded `require(riskLevel != RiskLevel.Unacceptable)`.
2. **High-risk agents must satisfy all four EU AI Act checks.** `humanOversight && technicalDoc && postMarket && incident` — no partial compliance for High risk.
3. **Auditor revocation does not automatically invalidate past attestations.** Revoking an auditor via `revokeAuditor()` prevents them from issuing new attestations but does not retroactively revoke existing ones.
4. **`isCompliant` cache can diverge from live state.** `expireAttestation()` must be called manually for the cache to update. `checkCompliance()` (view, not cached) always reflects the true live state.

---

## 13. ReputationDecay.sol

**File:** `vaultfire-keyprotocol/contracts/ReputationDecay.sol`
**Role:** ReputationDecay — time-based reputation decay engine with context-specific scoring and recovery bonuses.

### Full Source with Line Numbers

(Full source: 364 lines — see source files.)

### Line-by-Line Walkthrough

| Line range | Description |
|------------|-------------|
| 18–27 | **`DecayProfile` struct** — `baseScore` from the external registry, `decayedScore` is the live score, `lastActivityAt` is the key decay timestamp. |
| 29–34 | **`ContextReputation` struct** — per-context score (e.g., "trading", "code_review"). Initialized at 50 on first activity. |
| 36–43 | **`ActivityRecord` struct** — full audit entry for each recorded activity. |
| 55–58 | **Decay parameters** (configurable): `decayRatePerDay = 1` (1 point per day), `decayGracePeriod = 7 days` (no decay for first 7 days of inactivity), `minDecayedScore = 5` (floor), `recoveryBonusMax = 10` (max bonus from below-50 recovery). |
| 128–142 | **`initializeProfile()`** — seeds `baseScore` and `decayedScore` from the external reputation registry. Sets `lastActivityAt = block.timestamp`. If profile already exists, updates `baseScore` and `decayedScore` from registry. |
| 153–226 | **`recordActivity()`** — reporter-only. First applies pending decay via `_applyDecay()`. For positive activities: applies `weight` bonus plus optional recovery bonus. For negative activities: applies `weight × 2` penalty. Also updates context-specific score. Appends to `activityLog`. |
| 171–177 | **Recovery bonus logic**: if `decayedScore < 50` and the activity is positive, the recovery bonus is `(50 - decayedScore) × recoveryBonusMax / 50`. At score 0, bonus = `recoveryBonusMax` (max 10). At score 49, bonus ≈ 0.2. |
| 178 | `_min(decayedScore + boost, 100)` — caps score at 100. |
| 180–187 | Negative activity: `penalty = weight × 2`. If `decayedScore > penalty`, subtracts; otherwise clamps to `minDecayedScore`. |
| 238–267 | **`_applyDecay()`** — internal. Computes `timeSinceActivity = block.timestamp - lastActivityAt`. If within grace period: no decay, updates `lastDecayCalculation`. If beyond grace: `decayDays = (timeSinceActivity - gracePeriod) / 1 days`, `totalDecay = decayDays × decayRatePerDay`. Applies decay down to `minDecayedScore`. |
| 274–299 | **`getCurrentScore()`** — view-only computation of what the decayed score would be without mutating state. |

### Math: Reputation Decay Formula

**Full formula:**

\[ S(t) = \max\!\left(\, S_0 - \left\lfloor \frac{\max(0,\; t - t_{\text{last}} - t_{\text{grace}})}{86400} \right\rfloor \times r,\; S_{\text{min}} \right) \]

where:
- \(S_0\) = score at last activity
- \(t\) = current timestamp
- \(t_{\text{last}}\) = `lastActivityAt`
- \(t_{\text{grace}}\) = `decayGracePeriod` (default 7 days = 604,800 seconds)
- \(r\) = `decayRatePerDay` (default 1 point/day)
- \(S_{\text{min}}\) = `minDecayedScore` (default 5)

**Example over time (starting score = 80, defaults):**

| Day | Calculation | Score |
|-----|-------------|-------|
| 0 | Activity recorded | 80 |
| 7 | End of grace period | 80 |
| 8 | 1 day past grace | 80 − 1 = 79 |
| 14 | 7 days past grace | 80 − 7 = 73 |
| 30 | 23 days past grace | 80 − 23 = 57 |
| 75 | 68 days past grace | 80 − 68 = 12 |
| 82 | 75 days past grace | 80 − 75 = 5 (floor) |
| 100 | 93 days past grace | Stays at 5 (floor) |

**Recovery Bonus (score < 50):**

\[ \text{bonus} = \left\lfloor \frac{(50 - S) \times S_{\text{recov}}}{50} \right\rfloor \]

At \(S = 10\), \(S_{\text{recov}} = 10\): bonus = \(\lfloor 40 \times 10 / 50 \rfloor = 8\) points on top of `weight`.

**Context Score (no decay applied per-context):**

Context scores are updated by positive/negative activities with `weight` and `weight × 2` respectively. Context scores are not subject to the global time-decay — only the global `decayedScore` decays.

### State Variables

| Variable | Type | Purpose | Mutability |
|----------|------|---------|------------|
| `identityRegistry / reputationRegistry` | `immutable` | External registries | Immutable |
| `owner` | `address` | Admin | Transferable |
| `decayRatePerDay` | `uint256` | Points decayed per day of inactivity (1–10) | Mutable (owner) |
| `decayGracePeriod` | `uint256` | Inactivity grace period before decay starts | Mutable (owner) |
| `minDecayedScore` | `uint256` | Floor for decayed score (≤ 20) | Mutable (owner) |
| `recoveryBonusMax` | `uint256` | Max recovery bonus for low-score agents (≤ 20) | Mutable (owner) |
| `profiles` | `mapping(address => DecayProfile)` | Per-agent decay profiles | Mutable |
| `contextScores` | `mapping(address => mapping(bytes32 => ContextReputation))` | Per-agent per-context scores | Mutable |
| `activityLog` | `ActivityRecord[]` | Global activity audit log | Append-only |
| `reporters` | `mapping(address => bool)` | Authorized activity reporters | Mutable (owner) |

### Events

| Event | When fired | Logged fields |
|-------|-----------|---------------|
| `ActivityRecorded` | `recordActivity()` | `agent`, `context`, `positive`, `weight`, `reporter` |
| `ScoreDecayed` | `_applyDecay()` when score changes | `agent`, `oldScore`, `newScore`, `daysSinceActivity` |
| `ProfileCreated` | First `initializeProfile()` | `agent`, `baseScore` |
| `ReporterUpdated` | `setReporter()` | `reporter`, `authorized` |
| `DecayParametersUpdated` | `setDecayParameters()` | `ratePerDay`, `gracePeriod`, `minScore` |

### Security Invariants

1. **Activity reporters are strictly controlled.** Only authorized reporters can call `recordActivity()`. A compromised reporter can inflate or deflate scores.
2. **Decay is lazy.** Scores are not automatically decayed on every block. Decay is applied when `recordActivity()` or `applyDecay()` is called. The `getCurrentScore()` view function shows what the score would be without mutating state.
3. **Profiles must be initialized before recording activities.** `recordActivity()` requires `profile.exists` — `initializeProfile()` must be called first.
4. **Context scores are not decay-eligible.** Only the global `decayedScore` in `DecayProfile` undergoes time decay. Context scores (`ContextReputation.score`) only change through explicit `recordActivity()` calls.
5. **`baseScore` diverges from `decayedScore` over time.** `baseScore` reflects the last value read from the external reputation registry via `initializeProfile()`. `decayedScore` is the live VKP-internal score. Applications must query `getCurrentScore()` for the live value, not `profiles[agent].decayedScore` (which may be stale since the last `_applyDecay()` call).

---

## 14. MockIdentityRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockIdentityRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireIdentity` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireIdentity.sol";
     5
     6  /// @dev Mock identity registry for testing.
     7  contract MockIdentityRegistry is IVaultfireIdentity {
     8      mapping(address => bool) private _registered;
     9      mapping(address => uint256) private _registeredAt;
    10
    11     function register(address agent) external {
    12         _registered[agent] = true;
    13         _registeredAt[agent] = block.timestamp;
    14     }
    15
    16     function unregister(address agent) external {
    17         _registered[agent] = false;
    18     }
    17
    18     function getAgent(address agent) external view override returns (
    19         string memory agentURI, bool active, string memory agentType, uint256 registeredAt)
    20     {
    21         if (_registered[agent]) {
    22             return ('{"name":"mock-agent"}', true, "agent", _registeredAt[agent]);
    23         }
    24         return ("", false, "", 0);
    25     }
    26 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 7 | Inherits `IVaultfireIdentity` — implements the interface exactly, suitable as a drop-in test double. |
| 8–9 | Simple boolean + timestamp storage. No URI storage. |
| 11–14 | `register()` — permissionless in the mock (no access control), records `block.timestamp`. |
| 16–18 | `unregister()` — permissionless, sets `_registered = false`. |
| 18–25 | `getAgent()` returns a hardcoded JSON string `'{"name":"mock-agent"}'` for all registered addresses — sufficient for `bytes(agentURI).length > 0` checks. |

### Security Invariants

- **Test-only.** No access control on `register()` / `unregister()`. Never deploy in production.
- The returned `agentType = "agent"` and `active = true` match the `onlyRegisteredAgent` modifier check in `ScopedDelegation` and `AgentSLAEnforcer` that verify both `bytes(uri).length > 0 && active`.

---

## 15. MockBondsRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockBondsRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireBonds` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireBonds.sol";
     5
     6  contract MockBondsRegistry is IVaultfireBonds {
     7      struct Bond {
     8          address creator;
     9          uint256 stakeAmount;
    10         uint256 createdAt;
    11         bool active;
    12     }
    13
    14     mapping(uint256 => Bond) private _bonds;
    15     uint256 public bondCount;
    16
    17     function createBond(address creator, uint256 stakeAmount) external returns (uint256 bondId) {
    18         bondId = bondCount++;
    19         _bonds[bondId] = Bond({ creator: creator, stakeAmount: stakeAmount,
    20             createdAt: block.timestamp, active: true });
    21     }
    22
    23     function setBondStake(uint256 bondId, uint256 newStake) external {
    24         _bonds[bondId].stakeAmount = newStake;
    25     }
    26
    27     function deactivateBond(uint256 bondId) external { _bonds[bondId].active = false; }
    28     function activateBond(uint256 bondId) external { _bonds[bondId].active = true; }
    29
    30     function getBond(uint256 bondId) external view override returns (
    31         address creator, uint256 stakeAmount, uint256 createdAt, bool active)
    32     {
    33         Bond storage b = _bonds[bondId];
    34         return (b.creator, b.stakeAmount, b.createdAt, b.active);
    35     }
    36 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 17–21 | `createBond()` — allocates from `bondCount++`, starts active. |
| 23–25 | `setBondStake()` — allows tests to simulate stake reduction (testing `verifyTrust()` auto-revoke on bond drop). |
| 27–28 | `deactivateBond()` / `activateBond()` — toggle `active` flag for testing `_verifyTrustRequirements()`. |
| 30–35 | `getBond()` returns `(0, 0, 0, false)` for non-existent bonds — `getBond(999)` returns `creator = address(0)`, which fails the `creator != msg.sender` check in `DelegationRegistry.acceptDelegation()`. |

---

## 16. MockReputationRegistry.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/MockReputationRegistry.sol`
**Role:** Mock — in-memory implementation of `IVaultfireReputation` for test environments.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../IVaultfireReputation.sol";
     5
     6  contract MockReputationRegistry is IVaultfireReputation {
     7      struct Reputation {
     8          uint256 score;
     9          uint256 totalActions;
    10         uint256 lastUpdated;
    11     }
    12
    13     mapping(address => Reputation) private _reputations;
    14
    15     function setReputation(address agent, uint256 score, uint256 totalActions) external {
    16         _reputations[agent] = Reputation({
    17             score: score, totalActions: totalActions, lastUpdated: block.timestamp
    18         });
    19     }
    20
    21     function getReputation(address agent) external view override returns (
    22         uint256 score, uint256 totalActions, uint256 lastUpdated)
    23     {
    24         Reputation storage r = _reputations[agent];
    25         return (r.score, r.totalActions, r.lastUpdated);
    26     }
    27 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 15–19 | `setReputation()` — permissionless score setter. Allows test setup of arbitrary scores (0–100+) to exercise tier boundaries in `AgentInsurancePool` and threshold enforcement in `AgentSafetyManager`. |
| 21–26 | Returns `(0, 0, 0)` for agents with no set reputation — a zero score means high risk (tier 3) in `AgentInsurancePool` and triggers auto-freeze in `AgentSafetyManager` (since 0 ≤ freezeThreshold=10). |

---

## 17. ReentrancyAttacker.sol

**File:** `vaultfire-keyprotocol/contracts/mocks/ReentrancyAttacker.sol`
**Role:** Mock — adversarial contract proving `KeyVault.execute()` is reentrancy-safe.

### Full Source with Line Numbers

```solidity
     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.26;
     3
     4  import "../KeyVault.sol";
     5
     6  /// @dev Malicious contract that tries to re-enter KeyVault during execute()
     7  contract ReentrancyAttacker {
     8      KeyVault public target;
     9      uint256 public attackCount;
    10     bool public attacking;
    11
    12     constructor(address _target) {
    13         target = KeyVault(payable(_target));
    14     }
    15
    16     /// @dev When this contract receives ETH, it tries to re-enter execute()
    17     receive() external payable {
    18         if (attacking && attackCount < 3) {
    19             attackCount++;
    20             try target.execute(address(this), 0.01 ether, "") {
    21                 // If this succeeds, the reentrancy guard is broken
    22             } catch {
    23                 // Expected: should revert with "ReentrancyGuard: reentrant call"
    24             }
    25         }
    26     }
    27
    28     function startAttack() external { attacking = true; attackCount = 0; }
    29     function stopAttack() external { attacking = false; }
    30 }
```

### Line-by-Line Walkthrough

| Lines | Notes |
|-------|-------|
| 17–26 | `receive()` is triggered when `KeyVault.execute()` sends ETH to this contract. The attacker attempts up to 3 recursive `execute()` calls. Each attempt is wrapped in `try/catch` — the test verifies that all re-entrant calls are caught and reverted by `KeyVault`'s `nonReentrant` modifier. |
| 20 | The re-entrant call passes `value = 0.01 ether` — enough to exceed daily limits in combination with the outer call, so even if the guard failed, spending limits would catch it. The guard fires first. |
| 28–29 | `startAttack()` / `stopAttack()` allow test setup to arm and disarm the attacker. |

### Security Invariants Proven

- The `_ENTERED` flag set before `to.call{value: value}(data)` in `KeyVault.execute()` is still set when `receive()` is triggered in the attacker, causing the nested `execute()` call to revert with `"ReentrancyGuard: reentrant call"`.

---

## Math Reference

### SLA Penalty Math (AgentSLAEnforcer)

```
penalty = (remainingStake × penaltyRate × violationCount) / 10000
where:
  remainingStake  = stakedAmount - totalPenalties (running balance)
  penaltyRate     ∈ (0, 1000] basis points (0% < rate ≤ 10%)
  violationCount  ∈ {1, 2, 3}
  penalty         = min(penalty, remainingStake)  (cap at zero)
```

### Insurance Premium Math (AgentInsurancePool)

```
premium = (coverageLimit × rate × duration) / (365 days × 10000)
where:
  rate = 100 bp (tier 1), 250 bp (tier 2), or 500 bp (tier 3)
  tier determined by getReputation().score at policy creation time
```

### Reputation Decay Math (ReputationDecay)

```
S(t) = max(S₀ - floor((max(0, t - t_last - t_grace) / 86400)) × r, S_min)
where:
  S₀       = score at last activity
  t_grace  = decayGracePeriod (default 7 days)
  r        = decayRatePerDay (default 1 point/day)
  S_min    = minDecayedScore (default 5)
```

### Recovery Bonus Math (ReputationDecay)

```
recoveryBonus = floor((50 - S) × recoveryBonusMax / 50)   if S < 50
totalBoost    = weight + recoveryBonus
newScore      = min(S + totalBoost, 100)
```

### Scoped Delegation Expiry Math (ScopedDelegation)

```
For sub-delegation at time T₁ from parent with expiresAt = T₀ + D:
  max allowed duration = (T₀ + D) - T₁
  require(duration <= (parent.expiresAt - block.timestamp))
```

---

## Security Invariants

### Cross-Contract Invariants

1. **ERC-8004 is the universal trust anchor.** Every VKP contract gates access on `IVaultfireIdentity.getAgent()`. An unregistered address cannot own a `KeyVault`, create sessions, create delegations, or create scopes.

2. **Zero address is universally blocked.** All constructors and key functions check `!= address(0)`.

3. **CEI pattern in all value-transferring functions.** In `KeyVault.execute()`, `AgentInsurancePool.resolveClaim()`, `AgentSLAEnforcer.expireSLA()`, and `AgentInsurancePool.resolveClaim()`: state is updated before the `.call{value: ...}` interaction.

4. **External call failures use `try/catch` where auto-revocation semantics are required.** `DelegationRegistry._verifyTrustRequirements()` uses `try/catch` specifically to ensure a failing external call commits the revocation rather than rolling it back.

5. **Revocations are irreversible.** Session keys (`SessionKeyManager`), delegations (`DelegationRegistry`), and scopes (`ScopedDelegation`) can be revoked but never un-revoked. New instances must be created.

### Session Key Security

- Session keys expire by timestamp — no on-chain action needed; `validateSession()` reverts automatically.
- Lifetime `totalLimit` enforces a hard cap on aggregate spend regardless of time.
- Owner can revoke all sessions in a single transaction (`revokeAllSessions()`).
- `sessionToId` reverse map overwrites on address reuse — applications must use unique session addresses per session.

### Delegation Security

- Acceptance is two-phase: create (pending) → accept (active). Neither phase is usable without the other.
- `verifyTrust()` is permissionless: watchdog bots can enforce trust requirements without relying on the protocol team.
- Bond and reputation checks in `_verifyTrustRequirements()` use fail-closed `try/catch`.

### Replay Protection

- **Session keys:** `revoked` flag + timestamp + lifetime spend cap.
- **Delegations:** `revoked` flag + timestamp + `accepted` flag.
- **Scopes:** `revoked` + `active` flags + timestamp.
- **SLA reports:** `periodEnd <= block.timestamp` prevents future-dated reports; `periodEnd - periodStart >= MIN_REPORT_PERIOD` prevents zero-duration reports.

---

## Dependencies and Rebuild Recipe

### Dependency Graph

```
Interfaces (no dependencies):
  IVaultfireIdentity.sol
  IVaultfireBonds.sol
  IVaultfireReputation.sol

Core contracts:
  KeyVault.sol              ← IVaultfireIdentity
  VKPFactory.sol            ← KeyVault (and transitively IVaultfireIdentity)
  SessionKeyManager.sol     ← IVaultfireIdentity
  ScopedDelegation.sol      ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  DelegationRegistry.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentSLAEnforcer.sol      ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentInsurancePool.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  AgentSafetyManager.sol    ← IVaultfireIdentity, IVaultfireBonds, IVaultfireReputation
  ComplianceRegistry.sol    ← IVaultfireIdentity
  ReputationDecay.sol       ← IVaultfireIdentity, IVaultfireReputation

Mocks (test-only):
  MockIdentityRegistry.sol  ← IVaultfireIdentity
  MockBondsRegistry.sol     ← IVaultfireBonds
  MockReputationRegistry.sol← IVaultfireReputation
  ReentrancyAttacker.sol    ← KeyVault
```

### Deployment Order

Deployment order must respect the dependency graph. The three external registries from Batch 3 (ERC-8004) and Batch 2 (Bonds) must already be deployed before any VKP contract is deployed.

```
Step 1 — External prerequisites (already deployed):
  [Batch 3] ERC8004IdentityRegistry → IDENTITY_REGISTRY
  [Batch 2] AIAccountabilityBonds   → BONDS_REGISTRY
  [Batch 3] ERC8004ReputationRegistry → REPUTATION_REGISTRY

Step 2 — VKPFactory (and KeyVault implementation):
  VKPFactory(IDENTITY_REGISTRY)
  → agentVaults, allVaults, vaultCount initialized to empty/0
  → Per-agent KeyVaults deployed on demand via createVault()

Step 3 — SessionKeyManager:
  SessionKeyManager(IDENTITY_REGISTRY)
  → sessionCount = 0

Step 4 — ScopedDelegation:
  ScopedDelegation(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalScopes = 0, owner = deployer

Step 5 — DelegationRegistry:
  DelegationRegistry(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → delegationCount = 0

Step 6 — AgentSLAEnforcer:
  AgentSLAEnforcer(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalSLAs = 0, owner = deployer

Step 7 — AgentInsurancePool:
  AgentInsurancePool(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → totalPolicies = 0, arbiter = deployer, owner = deployer

Step 8 — AgentSafetyManager:
  AgentSafetyManager(IDENTITY_REGISTRY, BONDS_REGISTRY, REPUTATION_REGISTRY)
  → safeModeThreshold = 30, freezeThreshold = 10, owner = deployer

Step 9 — ComplianceRegistry:
  ComplianceRegistry(IDENTITY_REGISTRY)
  → totalAttestations = 0, owner = deployer

Step 10 — ReputationDecay:
  ReputationDecay(IDENTITY_REGISTRY, REPUTATION_REGISTRY)
  → decayRatePerDay = 1, decayGracePeriod = 7 days, minDecayedScore = 5

Post-deployment configuration:
  AgentSLAEnforcer.setMonitor(MONITOR_ADDRESS, true)
  AgentInsurancePool.setArbiter(ARBITER_MULTISIG)
  AgentSafetyManager.setSafetyOperator(SAFETY_OPERATOR, true)
  ComplianceRegistry.authorizeAuditor(AUDITOR_ADDRESS, AUDITOR_URI)
  ReputationDecay.setReporter(REPORTER_ADDRESS, true)
  ScopedDelegation.transferOwnership(GOVERNANCE_MULTISIG)
```

### Rebuild Checklist

- [ ] Interfaces deployed as `library`-style references or embedded in consuming contracts (no separate deployment needed for pure interfaces).
- [ ] `VKPFactory` is the canonical entry point for per-agent `KeyVault` deployment. Do not deploy `KeyVault` directly in production — use the factory for discoverability.
- [ ] `AgentInsurancePool.arbiter` should be a multi-sig from day one. The initial deployer-as-arbiter is a temporary convenience.
- [ ] `ComplianceRegistry.authorizeAuditor()` must be called before any attestations can be issued.
- [ ] `ReputationDecay` profiles must be initialized via `initializeProfile()` per agent before `recordActivity()` can be called.
- [ ] Mocks (`MockIdentityRegistry`, `MockBondsRegistry`, `MockReputationRegistry`, `ReentrancyAttacker`) must never appear in production deployment scripts. They are test-only and have no access controls.

---

*End of VaultfireKeyProtocol Technical Reference — Codex Batch 5*
