# Vaultfire Codex — Batch 2: V3 Bond Contracts

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

---

## Table of Contents

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

---

## Manifest Deduplication Notes

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

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

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

---

# Contract 1: BaseDignityBond

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

## Full Source Code

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

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

## Functions

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

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

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

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

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

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

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

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

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

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

---

# Contract 3: BaseYieldPoolBond (V2 — root workspace)

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

## What It Does

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

## Full Source Code

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

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

## Functions

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

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

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

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

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

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

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

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

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

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

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

## Math: Reserve Ratio

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

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

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

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

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

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

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

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

## What It Does

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

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

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

## Full Source Code

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

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

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

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

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

## What It Does

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

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

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

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

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

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

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

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

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

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

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

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

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

## Security Invariants Added by V3.3

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

## Rebuild Recipe

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

---

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

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

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

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

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

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

---

# Contract 7: AIAccountabilityBondsV2

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

## What It Does

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

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

## Full Source Code

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

Key sections are excerpted and annotated throughout this documentation section.

## Line-by-Line Walkthrough

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

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

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

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

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

### Structs

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

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

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

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

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

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

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

### Constructor (line ~290)

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

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

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

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

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

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

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

Returns the simple arithmetic average of the 6 latest metrics:

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

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

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

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

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

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

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

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

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

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

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

Core payout function. After 7-day timelock:

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

**Distribution math worked example:**

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

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

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

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

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

### Oracle and Verification System

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

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

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

## Math: Bond Value Formula

For V2:

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

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

**Distribution splits (V2):**

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

# Contract 8: AIAccountabilityBondsV2 (theloopbreaker-site variant)

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

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

---

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

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

## What It Does

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

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

## Detailed State Variables

Beyond the V2 set, V3 adds:

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

## Function Deep-Dives

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

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

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

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

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

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

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

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

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

V3.3 CEI order (from inline comments):

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

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

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

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

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

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

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

`_capToOracle(uint256 selfReported, bytes32 metricId)`:

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

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

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

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

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

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

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

## Math: Oracle Cross-Check

When `oracleEnforced = true`:

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

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

The averaging formula is:

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

## Events (V3 additions over V2)

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

## Security Invariants

All V2 invariants preserved, plus:

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

## Dependencies

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

## Rebuild Recipe

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

---

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

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

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

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

---

# Contract 11: AIAccountabilityBondsV3 (v3 directory)

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

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

---

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

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

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

---

# Contract 13: AIPartnershipBondsV2 (root workspace)

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

## What It Does

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

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

## Structs

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

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

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

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

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

## State Variables (key ones)

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

## Key Functions

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

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

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

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

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

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

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

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

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

Calls `_trackPendingDistribution`. Either participant can request distribution.

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

After 7-day timelock. Distribution formula:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## Math Summary for Partnership Bonds

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

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

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

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

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

# Contract 14: AIPartnershipBondsV2 (theloopbreaker-site)

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

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

---

# Contract 15: AIPartnershipBondsV3 (AIPartnershipBondsV3_to_push)

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

## What It Does

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

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

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

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

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

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

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

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

### Distribution formula (V3.1)

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

### `deactivateBond(uint256 bondId)`

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

---

# Contract 16: AIPartnershipBondsV3 (push_contracts)

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

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

---

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

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

## What It Does

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

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

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

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

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

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

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

### HIGH-05 Per-Token Partnership Fund

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

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

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

## Key State Variables (V3.3 additions)

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

## Rebuild Recipe

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

---

# Contract 18: AIPartnershipBondsV3 (v3 directory)

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

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

---

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

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

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

---

# Contract 20: AIPartnershipBondsV3 (test_fixes)

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

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

---

# Contract 21: VaultfireBondInsurancePool

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

## What It Does

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

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

## Full Source Code

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

## Functions

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

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

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

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

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

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

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

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

## Math: Claim Cap

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

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

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

## Events

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

## Security Invariants

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

## Dependencies

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

## Rebuild Recipe

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

---

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

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

## What It Does

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

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

## Full Source Code

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

No mutable storage at all.

## Security Invariants

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

## Events

None — this contract has no state-mutating functions.

## Dependencies

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

## Rebuild Recipe

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

---

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

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

## What It Does

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

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

## Key Test Structure

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

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

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

## Rebuild Recipe

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

---

# Contract 24: PartnershipBondsEnhanced

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

## What It Does

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

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

## Line-by-Line Walkthrough

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

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

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

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

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

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

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

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

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

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

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

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

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

## State Variables

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

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

## Security Invariants

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

## Events

None new. All events inherited from V2.

## Dependencies

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

## Rebuild Recipe

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

---

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

## Version Lineage

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

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

## Audit Finding → Fix Mapping

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

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

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

## Cross-Contract Data Flow

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

## Compiler Settings

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

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

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

## Token Address Reference (Production Chains)

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

---

## Glossary

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