MergeProof

Architecture#

System design document for the Mergeproof staked PR review protocol.

High-Level System Diagram#

                          ┌─────────────────────────────┐
                          │         Users / Agents       │
                          │  (Bounty owners, Submitters, │
                          │   Bug hunters, Attestors)    │
                          └──────┬──────────────┬────────┘
                                 │              │
                       TX 1 (stake)    TX 2 (action)
                                 │              │
                   ┌─────────────▼──┐   ┌───────▼────────────┐
                   │  Base (EVM)    │   │     GenLayer        │
                   │                │   │                     │
                   │  Escrow.sol    │◄──│  BountyRegistry.py  │
                   │  ├─ deposits   │   │  ├─ state machine   │
                   │  ├─ stakes     │RPC│  ├─ GitHub API reads│
                   │  └─ settle()   │   │  ├─ AI arbiter (v2) │
                   │                │   │  └─ payout calc     │
                   │  BridgeRx.sol  │   │                     │
                   │  └─ lzReceive  │   │  BridgeSender       │
                   │  └─ relay msg  │   │  └─ send_message()  │
                   └──────▲─────────┘   └──────┬──────────────┘
                          │                    │
                          │      ONE bridge    │
                          │      message per   │
                          │      bounty        │
                          │                    │
                   ┌──────┴────────────────────▼──────────┐
                   │           Bridge Layer               │
                   │                                      │
                   │  Path A: LayerZero V2 (production)   │
                   │  Path B: Relay service (dev/testnet) │
                   └──────────────────────────────────────┘

                   ┌──────────────────────────────────────┐
                   │           Client Layer               │
                   │                                      │
                   │  CLI  — mutations, two-TX orchestr.  │
                   │  Web  — read-only dashboard + CTAs   │
                   └──────────────────────────────────────┘

Design Principles#

  • Funds never leave Base until settlement — simpler security model
  • One bridge message per bounty — gas-efficient, atomic payouts
  • GenLayer is arbiter, not custodian — decides outcomes, doesn't hold funds
  • Stake-first model — users deposit on Base before calling GenLayer

Dual-Chain Design Rationale#

GenLayer: Logic and Arbitration#

GenLayer is an AI-native blockchain where intelligent contracts (Python) can:

  • Read external APIs — fetch GitHub CI status, PR state, merge status, and user profiles directly from contract code.
  • Use the equivalence principle — leader nodes fetch data, validator nodes independently verify. This gives decentralized consensus over external state.
  • Run AI evaluation (v2+) — LLMs evaluate bug validity, spec compliance, and severity as on-chain operations.

GenLayer is the state machine and arbiter. It tracks bounty lifecycle, decides outcomes, but never holds funds.

Base (EVM): Custody and Settlement#

Base is the EVM chain where:

  • Funds live permanently until settlement. The Escrow contract holds bounty deposits and stakes.
  • Settlement is atomic — one settle() call distributes all payouts for a bounty in a single transaction.
  • DeFi composability — ERC-20 tokens, battle-tested OpenZeppelin contracts, Foundry tooling.

Base is the custodian. It holds money and executes payouts, but never makes decisions about outcomes.

Why Two Chains?#

ConcernSingle-chainDual-chain (Mergeproof)
GitHub API accessRequires oracleNative in GenLayer contracts
AI arbitrationExternal service + trustOn-chain via GenLayer validators
Fund securityLogic + custody coupledCustody isolated on battle-tested EVM
Bridge complexityNoneOne message per bounty conclusion
Token supportDepends on chainAny ERC-20 on Base

The trade-off is bridge complexity, but the protocol minimizes this: only one bridge message per bounty lifetime (at conclusion).

The Stake-First Model#

Every action requiring economic commitment follows a two-transaction pattern:

Step 1: User deposits on Base (EVM)
──────────────────────────────────────
  Escrow.depositBounty()    or
  Escrow.depositStake()
  → Funds pulled from user, recorded in Escrow state

Step 2: User calls GenLayer
──────────────────────────────────────
  BountyRegistry.create_bounty()    or
  BountyRegistry.submit_pr()        or
  BountyRegistry.report_bug()       or
  BountyRegistry.attest()
  → Contract reads Base via eth_call RPC
  → Verifies deposit/stake exists and meets requirements
  → Only then proceeds with state change

Two-Transaction Pattern (all staked actions):

ActionTX 1 (Base)TX 2 (GenLayer)
Create bountydepositBounty(id, amount, pool, token)create_bounty(...) verifies deposit
Submit PRdepositStake(bountyId, stakeRatio%)submit_pr(...) verifies stake
Report bugdepositStake(bountyId, 0.25%)report_bug(...) verifies stake
AttestdepositStake(bountyId, 1%)attest(...) verifies stake

Single-Transaction Actions (no stake required): validate_bug, claim, abandon, cancel_bounty.

Why Stake-First?#

  1. Funds never leave Base until settlement. GenLayer never has custody.
  2. No trust in GenLayer for fund safety. Even if GenLayer has a bug, funds on Base are safe until a valid bridge message arrives.
  3. Proven pattern. Same eth_call RPC verification used in Rally for gas payments.
  4. Atomic from the user's perspective. The CLI handles both transactions in sequence.

Contract Architecture#

BountyRegistry.py (GenLayer)#

The core intelligent contract managing all protocol state.

State machine:

                    create_bounty()
           ┌───────────────────────────┐
           │                           ▼
         ┌─┴──┐    submit_pr()    ┌─────────┐
         │OPEN│──────────────────►│IN_REVIEW│
         └─┬──┘                   └────┬────┘
           │                           │
           │  cancel_bounty()          ├── report_bug()
           │  → CANCELLED              ├── validate_bug()
           │                           ├── attest()
           │                           │
           │                      ┌────┴────────┐
           │                      │             │
           │               claim()│        auto_reject()
           │                      │        abandon()
           │                      ▼             │
           │               ┌───────────┐        │
           │               │COMPLETED  │        │
           │               └───────────┘        │
           │                                    │
           └────────────────────────────────────┘
                    bounty reopens (OPEN)

Key responsibilities:

AreaWhat BountyRegistry Does
IdentityGitHub-to-wallet linking via challenge-response
DepositsVerifies Base escrow deposits via RPC (eth_call)
SubmissionsTracks PRs, commit hashes, review windows, attempt counts
Bug reportsRecords reports, manages validation status (pending/valid/invalid)
AttestationsEnforces 24h window, pool capacity limits, slashing
SettlementBuilds decision struct, sends ONE bridge message
GitHub readsCI status, PR author, merge status, issue existence

Storage layout (TreeMap-based):

# Identity
identities: TreeMap[Address, Identity]        # wallet → GitHub mapping
github_to_wallet: TreeMap[str, Address]       # github_user_id → wallet
pending_challenges: TreeMap[Address, str]     # wallet → challenge string

# Bounties
bounties: TreeMap[str, Bounty]                # bounty_id → Bounty
bounty_order: DynArray[str]                   # creation-order index (append-only)
status_counts: TreeMap[str, u256]             # O(1) status queries

# Submissions
submissions: TreeMap[str, Submission]         # submission_id → Submission
submitter_attempts: TreeMap[str, u256]        # "bountyId:githubId" → count

# Bug reports & attestations
bug_reports: TreeMap[str, BugReport]
submission_bugs: TreeMap[str, DynArray[str]]  # submission_id → bug IDs
attestations: TreeMap[str, Attestation]
submission_attestations: TreeMap[str, DynArray[str]]  # submission_id → att IDs

RPC stake verification:

Python
def verify_stake(self, bounty_id, staker, required):
    # DEV MODE: skip when bridge_sender is zero address
    if self.bridge_sender == Address("0x00...00"):
        return True

    # Manual ABI encoding (genvm_eth doesn't support bytes32)
    bounty_id_bytes = bounty_id.encode().ljust(32, b'\x00')[:32]
    staker_padded = bytes.fromhex(staker.as_hex[2:]).rjust(32, b'\x00')
    selector = bytes.fromhex("a7b9828f")  # stakes(bytes32,address)
    call_data = selector + bounty_id_bytes + staker_padded

    # Both leader and validators independently call Base RPC
    result_bytes = gl.eq_principle.strict_eq(
        lambda: self.request_to_rpc(base_rpc_url, escrow_address, call_data)
    )
    return int.from_bytes(result_bytes[:32], "big") >= required

Source: contracts/genlayer/BountyRegistry.py

Escrow.sol (Base / EVM)#

The source of truth for all funds. Holds deposits and stakes. Calculates and executes payouts.

Key state:

Solidity
mapping(bytes32 => BountyDeposit) public bountyDeposits;
mapping(bytes32 => mapping(address => uint256)) public stakes;

Settlement decision struct (received from GenLayer):

Solidity
struct SettlementDecision {
    bytes32 bountyId;
    Outcome outcome;          // CLAIM | REJECT | ABANDON | AUTO_REJECT
    address submitter;
    ValidBug[] validBugs;     // [{reporter, severity}]
    address[] invalidBugReporters;
    address[] validAttestors;
    address[] slashedAttestors;
}

Payout handlers by outcome:

OutcomeSubmitter StakeValid Bug ReportersInvalid ReportersAttestors (no bugs)Attestors (bugs found)Bounty
CLAIMReturnedReward + stake backSlashed → treasuryReward + stake backSlashed → treasuryMinus reductions → submitter
REJECTForfeitedReward + stake backSlashed → treasuryAll slashedFloor → owner
ABANDONForfeitedReward + stake backSlashed → treasuryAll slashedPool → owner; main stays
AUTO_REJECTForfeitedReward + stake backSlashed → treasuryAll slashedFloor + pool → owner

All payout math runs on-chain. GenLayer sends only the decision; Escrow calculates exact amounts from stored deposits, stakes, and protocol constants.

Source: contracts/evm/src/Escrow.sol

BridgeReceiver.sol (Base / EVM)#

Receives settlement messages on Base and routes them to Escrow. Supports two delivery paths:

  1. LayerZero V2 (lzReceive) — production path. Decodes LayerZero envelope, verifies trusted forwarder, extracts inner message.
  2. Relay service (processBridgeMessage) — dev/testnet path. Accepts messages directly from a trusted relayer address.

Format detection: Uses a 0x44454349 ("DECI") magic prefix to distinguish:

  • Decision format — new struct-based settlement (stripped prefix → ABI decode → Escrow.settle(decision))
  • Legacy format — pre-calculated payout arrays (used for cancellations: Escrow.settle(bountyId, payouts[]))

Source: contracts/evm/src/BridgeReceiver.sol

Cross-Chain Communication#

Settlement Flow#

1. Trigger: claim(), abandon(), auto_reject(), or cancel()

2. GenLayer builds SettlementDecision:
   ├─ bountyId
   ├─ outcome (CLAIM | REJECT | ABANDON | AUTO_REJECT)
   ├─ submitter address
   ├─ validBugs[] with severities
   ├─ invalidBugReporters[] (to slash)
   ├─ validAttestors[] (to reward)
   └─ slashedAttestors[]

3. BountyRegistry → BridgeSender.send_message()

4. Bridge delivers to Base:
   Production: LayerZero V2 → BridgeReceiver.lzReceive()
   Dev:        Relay polls  → BridgeReceiver.processBridgeMessage()

5. BridgeReceiver detects "DECI" prefix → decodes → Escrow.settle(decision)

6. Escrow executes batch payouts:
   ├─ Submitter: bounty (minus reductions) + stake return
   ├─ Valid bug reporters: severity-based reward + stake return
   ├─ Invalid reporters: stake slashed → treasury
   ├─ Valid attestors: fixed reward (0.5% of bounty) + stake return
   ├─ Slashed attestors: stake → treasury
   ├─ Owner: unused attestation pool
   └─ Treasury: protocol fees (10%) + slashed stakes

Bridge Delivery Paths#

Path 1: LayerZero V2 (production)
  GenLayer → BridgeSender → LayerZero DVNs → BridgeReceiver.lzReceive() → Escrow

Path 2: Relay Service (dev/testnet)
  GenLayer → BridgeSender → Relay polls get_message_hashes() →
  Relay translates JSON → ABI → BridgeReceiver.processBridgeMessage() → Escrow

Data Flow Diagrams#

Create Bounty#

Owner                   Base (Escrow)              GenLayer (BountyRegistry)
  │                         │                              │
  │  approve(escrow, amt)   │                              │
  ├────────────────────────►│                              │
  │                         │                              │
  │  depositBounty(id,      │                              │
  │    amt, pool, token)    │                              │
  ├────────────────────────►│                              │
  │                         │  records BountyDeposit       │
  │                         │                              │
  │  create_bounty(id, repo, issue, ...)                   │
  ├───────────────────────────────────────────────────────►│
  │                         │                              │
  │                         │◄── eth_call: getBountyDepositFull(id)
  │                         │──► returns (amt, pool, token, owner)
  │                         │                              │
  │                         │         fetch GitHub issue   │
  │                         │         create Bounty{open}  │
  │◄─────────────────────────────────────── return bountyId│

Submit PR#

Submitter               Base (Escrow)              GenLayer (BountyRegistry)
  │                         │                              │
  │  depositStake(id, amt)  │                              │
  ├────────────────────────►│                              │
  │                         │  stakes[id][sender] += amt   │
  │                         │                              │
  │  submit_pr(id, pr#, commit)                            │
  ├───────────────────────────────────────────────────────►│
  │                         │                              │
  │                         │◄── eth_call: stakes(id, sender)
  │                         │──► returns stake amount      │
  │                         │                              │
  │                         │    verify PR author = identity│
  │                         │    verify CI green (GitHub)   │
  │                         │    create Submission{active}  │
  │                         │    bounty → "in_review"       │
  │◄───────────────────────────────── return submission_id  │

Claim (Settlement)#

Submitter       GenLayer           Bridge           Base (Escrow)
  │                 │                 │                   │
  │  claim(id)      │                 │                   │
  ├────────────────►│                 │                   │
  │                 │                 │                   │
  │          verify window ended      │                   │
  │          verify no pending bugs   │                   │
  │          verify PR merged (GitHub)│                   │
  │                 │                 │                   │
  │          build SettlementDecision │                   │
  │                 │  send_message() │                   │
  │                 ├────────────────►│                   │
  │                 │                 │  settle(decision) │
  │                 │                 ├──────────────────►│
  │                 │                 │                   │
  │                 │                 │  batch transfers: │
  │                 │                 │   submitter       │
  │                 │                 │   bug reporters   │
  │                 │                 │   attestors       │
  │                 │                 │   treasury        │
  │                 │                 │   owner (unused)  │
  │  bounty.status = "completed"     │                   │
  │◄────────────────┤                 │                   │

Report Bug#

Hunter                  Base (Escrow)              GenLayer (BountyRegistry)
  │                         │                              │
  │  depositStake(id,       │                              │
  │    0.25% of bounty)     │                              │
  ├────────────────────────►│                              │
  │                         │  stakes[id][hunter] += amt   │
  │                         │                              │
  │  report_bug(id, commit, severity, description)         │
  ├───────────────────────────────────────────────────────►│
  │                         │                              │
  │                         │◄── eth_call: stakes(id, hunter)
  │                         │──► returns stake amount      │
  │                         │                              │
  │                         │    verify commit matches     │
  │                         │    verify window open        │
  │                         │    create BugReport{pending} │
  │◄─────────────────────────────────────── return bug_id  │

GitHub API Integration#

GenLayer's equivalence principle enables decentralized verification of external data.

Consensus Patterns#

strict_eq — Deterministic data. Used when fetched data IS the value and must match exactly between leader and validators:

  • Base RPC calls (eth_call for stake/deposit verification)
  • Profile README content (checking for challenge string)

run_nondet — Derived data. Used when raw API responses vary (timestamps, avatars) but derived facts are stable:

  • GitHub user lookup → extracts: id, login, bio
  • GitHub issue data → extracts: id, number, title, state
  • GitHub PR data → extracts: number, title, user.id, merged, state
  • CI check runs → compares derived pass/fail status (not raw arrays)

Stable Field Extraction#

GitHub API responses contain volatile fields that differ between requests. The contract extracts only stable fields:

Python
def leader_fn():
    response = gl.nondet.web.get(url)
    data = json.loads(response.body.decode("utf-8"))
    # Only stable fields — no timestamps, avatars, etc.
    return {
        "number": data["number"],
        "title": data["title"],
        "user": {"id": data["user"]["id"]},
        "merged": data.get("merged", False),
    }

def validator_fn(leaders_result):
    my_result = leader_fn()
    return my_result == leaders_result.calldata

CI Status Special Case#

CI check runs have an additional complication: new CI runs may start between leader/validator fetches. The contract compares derived status rather than raw arrays:

Python
def derive_status(checks):
    if not checks:
        return "pending"
    for c in checks:
        if c["status"] != "completed":
            return "pending"
        if c["conclusion"] != "success":
            return c["conclusion"]
    return "success"

# Validator compares: derive_status(leader_checks) == derive_status(my_checks)

Dev Mode#

When bridge_sender is the zero address (0x0000000000000000000000000000000000000000), the GenLayer contract enters dev mode:

CheckProductionDev Mode
GitHub issue fetchFetches via APIReturns mock data
PR author verificationFetches via APIReturns caller's identity
CI status checkFetches via APIReturns "success"
Bounty deposit verificationeth_call to BaseSkips, returns true
Stake verificationeth_call to BaseSkips, returns true
Identity registrationChallenge-response via GitHubdev_register_identity() — no GitHub
Review window minimum24 hoursConfigurable (can be 0)

Dev mode enables local development and testing against the GenLayer simulator without real GitHub repos, Base deposits, or LayerZero infrastructure.

Relay Service Architecture#

The relay (apps/relay/) bridges messages from GenLayer to Base for dev/testnet environments.

apps/relay/src/
├── index.ts     # Entry point: health server (port 8080), cron scheduler
├── relay.ts     # BridgeRelayService: polling loop, message delivery
├── decoder.ts   # Message translation: JSON → ABI encoding
└── config.ts    # Environment configuration loader

Polling loop (every 10 seconds):

1. Poll GenLayer: get_message_hashes()
2. Filter out already-processed hashes (in-memory Set)
3. For each new hash:
   a. Fetch message data from GenLayer
   b. Decode ABI envelope (srcChainId, srcSender, localContract, innerMessage)
   c. Parse JSON inner payload
   d. Re-encode as ABI for BridgeReceiver
   e. Call BridgeReceiver.processBridgeMessage()
   f. Mark hash as processed

Design choices:

  • Stateless — no database. Tracks processed messages in-memory via usedHashes Set. On restart, re-checks all messages (idempotent because Escrow rejects double-settlement via settled flag).
  • Translation layer — GenLayer emits JSON payloads; Base expects ABI-encoded data. The decoder handles the conversion including the "DECI" magic prefix for decision-based messages.
  • Health endpoint/health reports sync status, message counters, and staleness for monitoring.

Web Dashboard Architecture#

The web app (apps/web/) is a Next.js 15 application using the App Router.

apps/web/src/
├── app/                    # File-based routing
│   ├── layout.tsx          # Root layout, dark mode, header/footer
│   ├── page.tsx            # Landing page (marketing)
│   ├── bounties/page.tsx   # Bounty list with filters
│   ├── bounty/[id]/        # Bounty detail + state-aware CTAs
│   ├── submission/[id]/    # Submission with bug reports, attestations
│   ├── create/page.tsx     # Bounty creation guide (CLI commands)
│   └── how-it-works/       # Protocol explainer
├── components/             # Shared UI components
└── lib/
    ├── api.ts              # GenLayer read-only client, mock mode
    └── format.ts           # Display formatting helpers

Key design decisions:

  1. Read-only — no wallet connection, no write operations. All mutations via CLI. Reduces attack surface.
  2. Client-side data — contract reads happen in browser via genlayer-js. No server-side data fetching.
  3. Mock modeNEXT_PUBLIC_MOCK_MODE=true returns synthetic data for development without deployed contracts.
  4. State-aware CTAs — each bounty status shows different call-to-action panels with the exact CLI command to run.
  5. Map/BigInt normalization — GenLayer returns JavaScript Map and BigInt types. The api.ts layer converts to plain objects for React.

Styling: Tailwind CSS with CSS variable theming. Light/dark mode (system-aware, localStorage persisted). Responsive grid layouts.

CLI Architecture#

The CLI (apps/cli/) is the primary interface for all protocol mutations.

apps/cli/src/
├── index.ts                # Commander entry point, global options
├── commands/
│   ├── bounty/index.ts     # create, list, info, configure
│   ├── pr/index.ts         # submit, retry, claim, abandon, status
│   ├── bug/index.ts        # report, validate, list, info
│   ├── attest/index.ts     # submit, list
│   ├── identity/index.ts   # start, verify, info
│   ├── wallet/index.ts     # balance, faucet
│   └── config/index.ts     # get, set, list
└── lib/
    ├── genlayer.ts         # GenLayer client wrapper (callView/callWrite)
    ├── evm.ts              # EVM client, escrow interactions
    ├── github.ts           # GitHub API integration
    ├── output.ts           # Dual-mode output (TTY + JSON)
    ├── wallet.ts           # Key loading (env var, keystore)
    └── errors.ts           # Error classification + exit codes

Two-TX orchestration. The CLI handles the stake-first model transparently. Example for mergeproof bounty create:

1. Parse options, validate parameters
2. Get GenLayer + EVM clients
3. Check token balance
4. Confirm transfer (interactive or --yes)
5. ERC-20 approve → Escrow.depositBounty()
6. Wait for Base confirmation
7. BountyRegistry.create_bounty() → wait for GenLayer receipt
8. Post to GitHub issue (optional, with GITHUB_TOKEN)
9. Output result + next steps

Dual output mode:

  • TTY — colored output (chalk), spinners (ora), interactive confirmations, arrow-prefixed next-step hints.
  • JSON (--json) — structured output with { success, message, data, next_steps[] } for AI agent consumption.

Error handling: Classified error codes (INSUFFICIENT_FUNDS, NETWORK_ERROR, BOUNTY_NOT_FOUND) with semantic exit codes and retry hints.

Protocol Constants#

Defined identically in BountyRegistry (Python) and Escrow (Solidity):

ConstantValueDescription
PROTOCOL_FEE_BPS1000 (10%)Fee on bug rewards and attestation payouts
BOUNTY_FLOOR_BPS5000 (50%)Minimum bounty; triggers auto-reject if reached
MAX_ATTEMPTS3Maximum submission attempts per bounty per submitter
BUG_REPORT_STAKE_BPS25 (0.25%)Bug reporter stake as % of bounty
ATTESTATION_STAKE_BPS100 (1%)Attestor stake as % of bounty
ATTESTATION_REWARD_BPS50 (0.5%)Fixed reward per attestor (50% of stake)
ATTESTATION_WINDOW_HOURS24Attestation opens in last 24h of review window
SEVERITY_MINOR_BPS100 (1%)Bounty reduction for minor bug
SEVERITY_MAJOR_BPS300 (3%)Bounty reduction for major bug
SEVERITY_CRITICAL_BPS1000 (10%)Bounty reduction for critical bug

Monorepo Structure#

mergeproof/
├── apps/
│   ├── cli/                    # TypeScript CLI (@mergeproof/cli)
│   ├── web/                    # Next.js 15 dashboard
│   └── relay/                  # Bridge relay service
├── contracts/
│   ├── genlayer/               # Python intelligent contracts
│   │   ├── BountyRegistry.py   # Core protocol logic
│   │   └── BridgeSender.py     # Outbound bridge messages
│   └── evm/                    # Solidity contracts (Foundry)
│       └── src/
│           ├── Escrow.sol          # Fund custody + settlement
│           ├── BridgeReceiver.sol  # Inbound bridge messages
│           └── TestToken.sol       # ERC-20 for testing
├── packages/
│   └── contracts/              # Shared ABIs and type exports
├── tests/
│   ├── direct/                 # GenLayer direct-mode tests (fast, ~4s)
│   └── intelligent_contracts/  # GenLayer simulator-mode tests
├── scripts/                    # Deployment scripts
└── docs/
    ├── SPEC.md                 # Full protocol specification
    └── ARCHITECTURE.md         # This document

Build system: Turborepo with pnpm workspaces. TypeScript throughout (except Python contracts and Solidity).

Security Model#

Trust Boundaries#

ComponentTrustsNotes
Escrow.solBridge contract onlyonlyBridge modifier on settle()
BridgeReceiver.solLayerZero endpoint or trusted relayerVerifies forwarder or relayer address
BountyRegistry.pyGenLayer consensusNondeterministic reads validated by multiple nodes
Bug validation (v1)Bounty ownerOwner validates bug reports
Bug validation (v2+)AI arbiterGenLayer LLM evaluates bugs decentrally

Access Control#

  • Escrow.settle() — callable only by the bridge contract (onlyBridge)
  • BridgeReceiver.lzReceive() — callable only by LayerZero endpoint with trusted forwarder check
  • BridgeReceiver.processBridgeMessage() — callable only by trusted relayer
  • BountyRegistry.validate_bug() — callable only by the bounty owner (v1)
  • Admin functionsonlyOwner for Escrow config (bridge, treasury, chain ID) and BridgeReceiver config (forwarders, escrow, relayer)

Key Invariants#

  1. Zero-sum — no funds created or destroyed; every deposited token accounted for in payouts
  2. One settlement per bountysettled boolean prevents replay
  3. Stake-first ordering — GenLayer actions revert if Base deposits missing
  4. Bug reward = bounty reduction — rewards come from bounty reduction, preventing collusion
  5. Bounty floor (50%) — hitting floor triggers automatic rejection
  6. Collusion unprofitable — protocol fee (10%) makes coordinated attacks net-negative