MergeProof

Smart Contract Reference#

Mergeproof uses a dual-chain architecture: GenLayer handles state machine logic and GitHub verification, while Base (EVM) manages escrow, stakes, and fund settlement.

Table of Contents#

Architecture Overview#

User Wallet
    |
    ├── 1. Deposit funds/stake ──> Base Escrow (EVM)
    |
    ├── 2. Create bounty / Submit PR ──> GenLayer BountyRegistry
    |       (verifies deposit via eth_call RPC to Base)
    |
    └── 3. Claim / Settle
            GenLayer sends decision ──> Bridge ──> BridgeReceiver ──> Escrow.settle()

The stake-first model requires users to deposit funds on Base before taking actions on GenLayer. GenLayer verifies deposits via cross-chain eth_call RPC, and sends a single settlement message at bounty conclusion.

GenLayer -- BountyRegistry#

File: contracts/genlayer/BountyRegistry.py

The core intelligent contract managing the full bounty lifecycle: identity verification, bounty creation, PR submissions, bug reports, attestations, and settlement.

Data Structures#

Identity#

Maps a wallet address to a GitHub account.

Python
@dataclass
class Identity:
    github_user_id: str       # GitHub numeric user ID
    github_username: str      # GitHub login handle
    verified_at: str          # ISO 8601 timestamp

Bounty#

Represents a bounty attached to a GitHub issue.

Python
@dataclass
class Bounty:
    id: str                       # Unique bounty identifier
    repo: str                     # "owner/repo" format
    issue_id: u256                # GitHub issue number
    issue_title: str              # Issue title from GitHub
    atto_amount: u256             # Bounty amount (18 decimals)
    token: str                    # ERC-20 token contract address
    stake_ratio_bps: u256         # Stake ratio: 500-2500 (5-25%)
    atto_attestation_pool: u256   # Attestation reward pool
    review_window_hours: u256     # Review window duration
    status: str                   # open | in_review | completed | cancelled
    owner_github_id: str          # Bounty creator's GitHub ID
    owner_wallet: Address         # Bounty creator's wallet
    created_at: str               # ISO 8601 timestamp
    current_submission_id: str    # Active submission ID (empty if none)

Submission#

A PR submission against a bounty.

Python
@dataclass
class Submission:
    id: str                       # Submission identifier
    bounty_id: str                # Parent bounty ID
    pr_number: u256               # GitHub PR number
    pr_title: str                 # PR title
    commit_hash: str              # Locked commit hash
    submitter_github_id: str      # Submitter's GitHub ID
    submitter_wallet: Address     # Submitter's wallet
    atto_staked_amount: u256      # Stake amount
    attempt_number: u256          # 1-3 (max 3 attempts)
    status: str                   # active | merged | rejected | abandoned
    window_started_at: str        # ISO 8601 timestamp
    window_ends_at: str           # ISO 8601 timestamp
    atto_current_bounty: u256     # Bounty after bug reductions
    total_reduction_bps: u256     # Cumulative bug reduction in BPS

BugReport#

A bug report filed against a submission.

Python
@dataclass
class BugReport:
    id: str                       # Bug report identifier
    bounty_id: str                # Parent bounty ID
    submission_id: str            # Target submission ID
    commit_hash: str              # Must match submission's commit
    severity: str                 # minor | major | critical
    description: str              # Bug description
    evidence_url: str             # Link to evidence (gist, etc.)
    reporter_github_id: str       # Reporter's GitHub ID
    reporter_wallet: Address      # Reporter's wallet
    atto_staked_amount: u256      # Reporter's stake (0.25% of bounty)
    validation_status: str        # pending | valid | invalid
    created_at: str               # ISO 8601 timestamp
    validated_at: str             # ISO 8601 timestamp (empty if pending)

Attestation#

A "clean code" attestation for a submission.

Python
@dataclass
class Attestation:
    id: str                       # Attestation identifier
    bounty_id: str                # Parent bounty ID
    submission_id: str            # Target submission ID
    attestor_github_id: str       # Attestor's GitHub ID
    attestor_wallet: Address      # Attestor's wallet
    atto_staked_amount: u256      # Attestor's stake (1% of bounty)
    created_at: str               # ISO 8601 timestamp
    slashed: bool                 # Whether stake was slashed

State Machine#

Bounties follow this lifecycle:

                    ┌──────────────────────────────┐
                    │                              │
                    v                              │
  create_bounty ──> OPEN ──> submit_pr ──> IN_REVIEW
                    ^  │                      │
                    │  │                      ├── claim ──> COMPLETED
                    │  │                      ├── abandon ──> OPEN (reopened)
                    │  │                      ├── auto_reject ──> OPEN (reopened)
                    │  │                      └── retry ──> OPEN ──> IN_REVIEW
                    │  │
                    │  └── cancel_bounty ──> CANCELLED
                    │
                    └── (reopened after abandon/reject)

State transitions:

FromToTriggerWho
(new)opencreate_bounty()Bounty owner
openin_reviewsubmit_pr()Any verified user
in_reviewcompletedclaim()Submitter (after PR merged)
in_reviewopenabandon()Submitter (forfeits stake)
in_reviewopen_auto_reject()System (bounty floor reached)
in_reviewopenretry()Submitter (new commit)
opencancelledcancel_bounty()Bounty owner

Protocol Constants#

ConstantValueDescription
PROTOCOL_FEE_BPS1000 (10%)Fee on bug rewards and attestation payouts
BOUNTY_FLOOR_BPS5000 (50%)Minimum bounty value before auto-reject
MAX_ATTEMPTS3Maximum submission attempts per user per bounty
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%)Attestor reward as % of bounty
ATTESTATION_WINDOW_HOURS24Attestation opens in last 24h of review
MIN_WINDOW_HOURS24Minimum review window (production)
MAX_WINDOW_HOURS168 (7 days)Maximum review window

Bug severity reductions:

SeverityReduction (BPS)Bounty Impact
minor1001%
major3003%
critical100010%

Public Methods#

Identity#

start_identity_verification() -> str#

Generate a challenge string for GitHub identity verification. User must post this to their GitHub bio or profile README.

  • Access: Write (any wallet)
  • Returns: Challenge string (format: mergeproof-verify:{wallet}:nonce:{nonce})
verify_identity(github_username: str)#

Complete identity verification by checking the challenge on GitHub.

  • Access: Write (any wallet)
  • Params: github_username - GitHub login handle
  • Checks: Challenge present in GitHub bio or profile README
  • Reverts: If challenge not found, or GitHub ID already linked to different wallet
get_identity(wallet: str) -> dict#

Get verified identity for a wallet address.

  • Access: View
  • Returns: {github_user_id, github_username, verified_at} or {}
dev_register_identity(github_username: str) -> str#

Dev mode only. Register identity without GitHub verification. Only works when bridge_sender is the zero address.

  • Access: Write
  • Reverts: If not in dev mode

Bounty Management#

create_bounty(bounty_id, repo, issue_id, atto_amount, token, stake_ratio_bps, atto_attestation_pool, review_window_hours) -> str#

Create a bounty for a GitHub issue.

  • Access: Write (verified wallet)
  • Prerequisite: Escrow.depositBounty() on Base
  • Params:
    • bounty_id: str - Unique identifier (must match Base deposit)
    • repo: str - GitHub repo (owner/repo format)
    • issue_id: int - GitHub issue number
    • atto_amount: int - Bounty amount in token's smallest unit
    • token: str - ERC-20 token address
    • stake_ratio_bps: int - Stake ratio 500-2500 (5-25%)
    • atto_attestation_pool: int - Attestation pool amount (0 disables attestations)
    • review_window_hours: int - Review window (24-168 hours)
  • Validates: Stake ratio range, window range, deposit on Base, issue exists on GitHub
  • Returns: The bounty ID
get_bounty(bounty_id: str) -> dict#

Get bounty details.

  • Access: View
  • Returns: Bounty dict or {}
list_bounties(status, token, offset, limit, sort) -> dict#

List bounties with filtering and pagination.

  • Access: View
  • Params:
    • status: str - Filter by status (optional)
    • token: str - Filter by token address (optional)
    • offset: int - Items to skip (default: 0)
    • limit: int - Max items (default: 50)
    • sort: str - "newest" or "oldest" (default: "newest")
  • Returns: {items: [...], total: int, has_more: bool}
get_bounty_stats() -> dict#

Get bounty counts by status (O(1) operation).

  • Access: View
  • Returns: {total, open, in_review, completed, cancelled}
cancel_bounty(bounty_id: str)#

Cancel an open bounty and refund the owner. Cannot cancel if there is an active submission.

  • Access: Write (bounty owner only)
  • Triggers: Settlement via bridge (full refund)

Submissions#

submit_pr(bounty_id: str, pr_number: int, commit_hash: str) -> str#

Submit a PR for a bounty. First-come-first-served.

  • Access: Write (verified wallet)
  • Prerequisite: Escrow.depositStake() with bounty_amount * stake_ratio_bps / 10000
  • Validates: Bounty is open, no active submission, attempt limit, stake on Base, PR author matches identity, CI is green
  • Returns: Submission ID
retry(bounty_id: str, new_commit_hash: str) -> str#

Retry with a new commit after fixing bugs. Review window must have ended.

  • Access: Write (original submitter only)
  • Prerequisite: Stake on Base (may need top-up if slashed)
  • Validates: Window ended, attempt limit, CI green
  • Returns: New submission ID
claim(bounty_id: str)#

Claim bounty after PR is merged. Review window must have ended and all bug reports must be validated.

  • Access: Write (submitter only)
  • Validates: Window ended, no pending bugs, PR merged on GitHub
  • Triggers: Settlement with CLAIM outcome
abandon(bounty_id: str)#

Abandon submission and forfeit stake. Bounty reopens for others.

  • Access: Write (submitter only)
  • Triggers: Settlement with ABANDON outcome
get_submission(submission_id: str) -> dict#

Get submission details.

  • Access: View

Bug Reports#

report_bug(bounty_id, commit_hash, severity, description, evidence_url) -> str#

Report a bug against the active submission.

  • Access: Write (verified wallet)
  • Prerequisite: Escrow.depositStake() with 0.25% of bounty
  • Params:
    • bounty_id: str - Target bounty
    • commit_hash: str - Must match active submission's commit
    • severity: str - "minor" | "major" | "critical"
    • description: str - Bug description
    • evidence_url: str - Link to evidence (optional)
  • Validates: Review window open, commit hash matches, severity valid, stake on Base
  • Returns: Bug report ID
validate_bug(bug_id: str, is_valid: bool, new_severity: str = "")#

Validate a bug report. Bounty owner only (v1).

  • Access: Write (bounty owner)
  • Params:
    • bug_id: str - Bug report ID
    • is_valid: bool - Whether the bug is valid
    • new_severity: str - Optional severity rescope
  • Side effects: If valid, reduces bounty. If total reduction >= 50% (floor), triggers auto-reject.
get_bug_reports(submission_id: str) -> list[dict]#

Get all bug reports for a submission.

  • Access: View

Attestations#

attest(bounty_id: str) -> str#

Attest that the submission code looks good. Only available in the last 24 hours of the review window.

  • Access: Write (verified wallet)
  • Prerequisite: Escrow.depositStake() with 1% of bounty
  • Validates: Attestation pool not zero, window open (last 24h), not already attested, pool can support another attestor, stake on Base
  • Returns: Attestation ID
get_attestations(submission_id: str) -> list[dict]#

Get all attestations for a submission.

  • Access: View

GitHub API Integration#

BountyRegistry uses GenLayer's equivalence principle for consensus on external data. Each GitHub API call uses a leader/validator pattern:

  1. Leader node fetches data from GitHub and extracts stable fields (IDs, titles, merge status)
  2. Validator nodes independently fetch the same data and compare results
  3. Only fields that are stable across fetches are compared (e.g., user ID, not timestamps)

API calls made:

  • GET /users/{username} - Identity verification (user ID, login, bio)
  • GET /repos/{owner}/{repo}/issues/{id} - Issue existence check
  • GET /repos/{owner}/{repo}/pulls/{id} - PR verification (author, merge status)
  • GET /repos/{owner}/{repo}/commits/{hash}/check-runs - CI status check
  • GET raw.githubusercontent.com/{user}/{user}/main/README.md - Profile README

RPC verification uses gl.eq_principle.strict_eq() for Base chain calls (deposit/stake verification), ensuring byte-exact matching between leader and validator results.

Dev Mode#

When bridge_sender is the zero address (0x000...000), the contract enters dev mode:

  • Identity: dev_register_identity() available (no GitHub verification)
  • Stake verification: All verify_bounty_deposit() and verify_stake() calls return True
  • GitHub API: All API calls return mock data (issues, PRs, CI)
  • Bridge: Settlement messages are still constructed but sent to the zero address

Dev mode is used for local testing and development where external services are unavailable.

EVM -- Escrow#

File: contracts/evm/src/Escrow.sol

Manages all funds on Base: bounty deposits, stakes, and settlement payouts. Inherits from OpenZeppelin's Ownable and ReentrancyGuard.

Types and Structs#

Outcome (enum)#

Solidity
enum Outcome {
    CLAIM,       // PR merged, submitter receives bounty
    REJECT,      // PR not merged, stake forfeited
    ABANDON,     // Submitter abandoned, stake forfeited
    AUTO_REJECT  // Bounty floor reached, auto-rejected
}

Severity (enum)#

Solidity
enum Severity {
    MINOR,    // 1% bounty reduction
    MAJOR,    // 3% bounty reduction
    CRITICAL  // 10% bounty reduction
}

SettlementDecision#

The decision struct sent from GenLayer via the bridge.

Solidity
struct SettlementDecision {
    bytes32 bountyId;
    Outcome outcome;
    address submitter;
    ValidBug[] validBugs;
    address[] invalidBugReporters;
    address[] validAttestors;
    address[] slashedAttestors;
}

BountyDeposit#

Solidity
struct BountyDeposit {
    uint256 amount;           // Main bounty amount
    uint256 attestationPool;  // Attestation reward pool
    address token;            // ERC-20 token address
    address owner;            // Bounty creator
    bool settled;             // Whether already settled
}

ValidBug#

Solidity
struct ValidBug {
    address reporter;
    Severity severity;
}

Deposit Functions#

Called by users before GenLayer actions.

depositBounty(bytes32 bountyId, uint256 amount, uint256 attestationPool, address token)#

Deposit bounty funds with a separate attestation pool.

  • Access: Public (anyone)
  • Requires: amount > 0, token != address(0), bounty ID not already used
  • Token transfer: amount + attestationPool via safeTransferFrom
  • Emits: BountyDeposited(bountyId, owner, token, amount, attestationPool)

depositBounty(bytes32 bountyId, uint256 amount, address token)#

Legacy variant. Entire amount treated as bounty, attestation pool is 0.

depositStake(bytes32 bountyId, uint256 amount)#

Deposit stake for a bounty (before submit_pr, report_bug, or attest on GenLayer).

  • Access: Public (anyone)
  • Requires: amount > 0, bounty deposit exists
  • Token: Uses same token as the bounty deposit
  • Stakes are cumulative: Multiple deposits add to the staker's total
  • Emits: StakeDeposited(bountyId, staker, amount)

Settlement Functions#

Called exclusively by the bridge.

settle(SettlementDecision calldata decision, uint32 sourceChainId)#

Execute settlement with on-chain payout calculation. All math happens in Escrow based on the decision struct.

  • Access: onlyBridge modifier
  • Requires: Source chain matches trusted chain, bounty exists, not already settled
  • Emits: Settled(bountyId, outcome, totalPaid) plus individual PayoutExecuted events

settle(bytes32 bountyId, uint32 sourceChainId, Payout[] calldata payouts)#

Legacy settlement with pre-calculated payouts. Used for cancellations.

  • Access: onlyBridge modifier

View Functions#

Called by GenLayer via eth_call RPC for cross-chain verification.

getBountyDeposit(bytes32 bountyId) -> (uint256 amount, address token, address owner)#

Basic deposit info.

getBountyDepositFull(bytes32 bountyId) -> (uint256 amount, uint256 attestationPool, address token, address owner, bool settled)#

Full deposit info including attestation pool and settlement status.

getStake(bytes32 bountyId, address staker) -> uint256#

Stake amount for a specific staker on a bounty.

Internal Settlement Handlers#

Each outcome has a dedicated handler with specific payout logic:

_handleClaim -- PR Merged Successfully#

RecipientReceivesCondition
SubmitterBounty minus bug reductionsAlways
SubmitterStake returnedAlways
Valid bug reportersBug reward (severity-based) + stake returnedIf bugs validated
Invalid bug reportersStake forfeited (to treasury)If bugs invalidated
Attestors (no bugs)Fixed reward (0.5% of bounty) + stake returnedNo valid bugs found
Attestors (bugs found)Stake forfeited (to treasury)Valid bugs found
Bounty ownerUnused attestation pool returnedAlways

_handleReject -- PR Not Merged#

RecipientReceives
Bounty ownerBounty floor (50%) + attestation pool
Valid bug reportersBug rewards + stake returned
Invalid bug reportersStake forfeited
SubmitterStake forfeited
AttestorsStake forfeited

_handleAbandon -- Submitter Abandoned#

RecipientReceives
Bounty ownerAttestation pool returned
Valid bug reportersBug rewards + stake returned
Invalid bug reportersStake forfeited
SubmitterStake forfeited
AttestorsStake forfeited
Main bountyStays in escrow (bounty reopens)

_handleAutoReject -- Bounty Floor Reached#

RecipientReceives
Bounty ownerFloor amount (50%) + attestation pool
Valid bug reportersBug rewards + stake returned
Invalid bug reportersStake forfeited
SubmitterStake forfeited
AttestorsStake forfeited

Admin Functions#

All require onlyOwner:

  • setBridge(address _bridge) - Set the BridgeReceiver contract
  • setTreasury(address _treasury) - Set the treasury address for protocol fees
  • setTrustedSourceChain(uint32 _chainId) - Set the GenLayer chain ID

Events#

Solidity
event BountyDeposited(bytes32 indexed bountyId, address indexed owner, address token, uint256 amount, uint256 attestationPool);
event StakeDeposited(bytes32 indexed bountyId, address indexed staker, uint256 amount);
event Settled(bytes32 indexed bountyId, Outcome outcome, uint256 totalPaid);
event PayoutExecuted(bytes32 indexed bountyId, address indexed recipient, uint256 amount, PayoutType payoutType);
event BridgeUpdated(address indexed oldBridge, address indexed newBridge);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);
event TrustedSourceChainUpdated(uint32 indexed oldChainId, uint32 indexed newChainId);

EVM -- BridgeReceiver#

File: contracts/evm/src/BridgeReceiver.sol

Receives cross-chain settlement messages from GenLayer and routes them to the Escrow contract. Supports both LayerZero V2 and a direct trusted relayer interface.

Message Flow#

GenLayer BountyRegistry
    |
    v
BridgeForwarder (GenLayer)
    |
    ├── LayerZero V2 ──> lzReceive() ──> Escrow.settle()
    |
    └── Trusted Relayer ──> processBridgeMessage() ──> Escrow.settle()

Message Format#

LayerZero envelope:

abi.encode(srcChainId, srcSender, localContract, innerMessage)

Decision format (new): Inner message prefixed with 0x44454349 ("DECI" magic bytes), followed by ABI-encoded SettlementDecision struct.

Legacy format: Inner message is abi.encode(bountyId, payouts[]).

The receiver auto-detects the format by checking for the "DECI" prefix.

Interface#

File: contracts/evm/src/interfaces/IGenLayerBridgeReceiver.sol

Solidity
interface IGenLayerBridgeReceiver {
    function processBridgeMessage(
        uint32 _sourceChainId,
        address _sourceContract,
        bytes calldata _message
    ) external;
}

BridgeReceiver Admin Functions#

All require onlyOwner:

  • setTrustedForwarder(uint32 _remoteEid, bytes32 _remoteForwarder) - Set trusted remote forwarder for a chain
  • removeTrustedForwarder(uint32 _remoteEid) - Remove trusted forwarder
  • setEscrow(address _escrow) - Set the Escrow contract
  • setTrustedRelayer(address _relayer) - Set trusted relayer for direct message delivery

Entry Points#

lzReceive(Origin, bytes32, bytes, address, bytes) -- LayerZero#

  • Access: LayerZero endpoint only (msg.sender == endpoint)
  • Validates: Trusted forwarder, escrow set
  • Decodes: Bridge envelope, then routes to decision or legacy handler

processBridgeMessage(uint32, address, bytes) -- Direct Relayer#

  • Access: Trusted relayer only (msg.sender == trustedRelayer)
  • Validates: Escrow set
  • Decodes: Inner message directly (no envelope)

Cross-Chain Settlement Flow#

  1. User action on GenLayer triggers settlement (claim, abandon, auto-reject, cancel)
  2. BountyRegistry constructs a SettlementDecision with:
    • Outcome type (CLAIM/REJECT/ABANDON/AUTO_REJECT)
    • Submitter address
    • List of valid bugs (reporter + severity)
    • List of invalid bug reporters (to slash)
    • List of valid/slashed attestors
  3. Bridge delivers message to BridgeReceiver on Base
  4. BridgeReceiver decodes and calls Escrow.settle(decision, sourceChainId)
  5. Escrow calculates all payouts on-chain using the decision + protocol constants
  6. Funds distributed via safeTransfer with events emitted per payout

Security Model#

Stake Verification Flow#

All actions requiring economic commitment follow the same pattern:

  1. User deposits funds on Base Escrow (depositBounty or depositStake)
  2. User calls GenLayer method (create_bounty, submit_pr, report_bug, attest)
  3. GenLayer verifies deposit via eth_call RPC to Base Escrow
  4. Action proceeds only if deposit meets minimum requirements

Bridge Trust Assumptions#

  • Escrow trusts only the bridge address (BridgeReceiver) to call settle()
  • BridgeReceiver trusts only the LayerZero endpoint or the trusted relayer
  • BridgeReceiver validates the remote forwarder is trusted for the source chain
  • Escrow validates the sourceChainId matches the configured trusted chain
  • A single compromised bridge message could execute arbitrary payouts within a bounty's deposited funds

Access Control Patterns#

ContractPatternDetails
EscrowOwnableAdmin sets bridge, treasury, trusted chain
EscrowonlyBridgeOnly BridgeReceiver can call settle()
EscrowReentrancyGuardAll deposit and settlement functions
BridgeReceiverOwnableAdmin sets forwarder, escrow, relayer
BridgeReceiverOnlyEndpointLayerZero messages must come from endpoint
BridgeReceiverOnlyRelayerDirect messages must come from trusted relayer
BountyRegistryIdentity checkWrite methods require verified GitHub identity
BountyRegistryOwner checkvalidate_bug, cancel_bounty restricted to bounty owner

Token Safety#

  • All ERC-20 transfers use OpenZeppelin's SafeERC20 library
  • Deposit amounts validated as non-zero
  • Settlement marks bounties as settled to prevent double-settlement

Deployment#

Deployed contract addresses are managed through deployment configuration. See deployment configs for current addresses.

Deploy Sequence#

  1. Deploy EVM contracts (Base):

    Bash
    cd contracts/evm && forge script script/Deploy.s.sol --broadcast
    • Deploy Escrow(treasury, owner)
    • Deploy BridgeReceiver(lzEndpoint, owner)
    • Configure: escrow.setBridge(bridgeReceiver), escrow.setTrustedSourceChain(genLayerChainId)
    • Configure: bridgeReceiver.setEscrow(escrow), bridgeReceiver.setTrustedForwarder(...)
  2. Deploy GenLayer contract:

    Bash
    # Via CLI or GenLayer deployment tools
    • Deploy BountyRegistry(baseRpcUrl, escrowAddress, bridgeSender, chainId, treasury)
  3. Verify cross-chain connectivity:

    • GenLayer can read Escrow state via RPC
    • Bridge can deliver messages from GenLayer to BridgeReceiver

Testing#

GenLayer Contract Tests (Direct Mode)#

Fast Python tests (~ms) that run contract logic directly without the simulator.

Bash
pnpm test:direct
# or: python -m pytest tests/direct/ -v

Tests use dev mode (bridge_sender = 0x0...0) which skips external calls:

Python
# conftest.py fixture
dev_bounty_registry = direct_deploy(
    bounty_contract_path,
    bridge_sender="0x0000000000000000000000000000000000000000",
    ...
)

Test coverage:

  • Identity registration (dev mode and production mode rejection)
  • Bounty creation with parameter validation
  • PR submission flow (first-come-first-served)
  • Bug reporting and validation (valid/invalid)
  • Bounty floor auto-reject mechanism
  • Bounty cancellation (owner only, no active submissions)
  • Submission abandonment

EVM Contract Tests (Foundry)#

Solidity tests using Forge with mock contracts.

Bash
cd contracts/evm && forge test

Test setup uses mock ERC-20 token (6 decimals like USDC) and standard amounts:

  • Bounty: $10,000
  • Attestation pool: $500
  • Submitter stake: $1,000 (10%)
  • Bug report stake: $25 (0.25%)
  • Attestation stake: $100 (1%)

Test coverage:

  • Deposit functions (bounty and stake)
  • Settlement outcomes (CLAIM, REJECT, ABANDON, AUTO_REJECT)
  • Payout calculations (bug rewards, attestor rewards, protocol fees)
  • Access control (onlyBridge, onlyOwner)
  • Edge cases (double settlement, insufficient funds)