Skip to main content

Security Model

NullGraph implements multiple layers of security controls at both the on-chain program and frontend levels. This page documents the security architecture, protection mechanisms, and implementation details.
NullGraph is a hackathon/demo project and has not undergone formal security audits. Do not use in production without comprehensive security review and testing.

On-Chain Security Architecture

The Solana program (lib.rs) implements defense-in-depth security controls across all instructions and account operations.

Signer Authentication

Every write instruction enforces strict signer validation through Anchor’s account struct constraints:
#[account(mut)]
pub researcher: Signer<'info>,
All state-modifying operations require valid transaction signatures from the appropriate authority. No instruction accepts unsigned transactions. Locations in code:
  • lib.rs:283 - InitializeProtocol
  • lib.rs:303 - SubmitNullResult
  • lib.rs:331 - CreateBounty
  • lib.rs:376 - SubmitToBounty
  • lib.rs:405 - ApproveBountySubmission
  • lib.rs:445 - CloseBounty

PDA Ownership Validation

All data accounts are program-owned PDAs (Program Derived Addresses) with deterministic seeds. This prevents:
  • Account spoofing attacks
  • Cross-program account injection
  • Unauthorized account creation
PDA Seed Structures:
AccountSeedsLocation
ProtocolState["protocol_state"]lib.rs:289
NullResult["null_result", researcher_pubkey, specimen_number]lib.rs:316-320
NullBounty["null_bounty", creator_pubkey, bounty_number]lib.rs:344-348
BountyVault["bounty_vault", bounty_pda]lib.rs:358
BountySubmission["bounty_submission", bounty_pda, null_result_pda]lib.rs:390-394
All PDAs use the init constraint, which automatically validates that accounts don’t already exist and are owned by the program.

Authorization Constraints

The program enforces ownership and authority checks using Anchor’s has_one constraint:
#[account(
    has_one = researcher,  // lib.rs:379
)]
pub null_result: Account<'info, NullResult>,
#[account(
    mut,
    has_one = creator,  // lib.rs:409, lib.rs:449
)]
pub bounty: Account<'info, NullBounty>,
Protection guarantees:
  • Only the NKA researcher can submit it to bounties (lib.rs:379)
  • Only the bounty creator can approve submissions (lib.rs:409)
  • Only the bounty creator can close bounties and reclaim funds (lib.rs:449)

State Machine Guards

All instructions validate current state before allowing transitions:
// Bounty must be Open for submissions
require!(bounty_status == 0, NullGraphError::InvalidBountyStatus);  // lib.rs:125

// Bounty must be Matched for approval
require!(bounty.status == 1, NullGraphError::InvalidBountyStatus);  // lib.rs:157

// Submission must be Pending for approval
require!(submission.status == 0, NullGraphError::InvalidSubmissionStatus);  // lib.rs:164

// Bounty must be Open or Matched for closure
require!(
    bounty.status == 0 || bounty.status == 1,
    NullGraphError::InvalidBountyStatus
);  // lib.rs:233-236
Status Definitions:
Account TypeStatusValueAllowed Transitions
NullResultPending0→ Verified, Disputed
NullResultVerified1Final state
NullResultDisputed2Final state
NullBountyOpen0→ Matched, Closed
NullBountyMatched1→ Fulfilled, Closed
NullBountyFulfilled2Final state
NullBountyClosed3Final state
BountySubmissionPending0→ Approved, Rejected
BountySubmissionApproved1Final state
BountySubmissionRejected2Final state

Replay Attack Prevention

The PDA init constraint provides automatic replay protection:
#[account(
    init,  // Fails if account already exists
    payer = researcher,
    space = 8 + BountySubmission::INIT_SPACE,
    seeds = [
        b"bounty_submission",
        bounty.key().as_ref(),
        null_result.key().as_ref(),
    ],
    bump,
)]  // lib.rs:387-396
Protection mechanisms:
  • Each NKA can only be submitted to a bounty once (unique PDA per bounty-NKA pair)
  • Researchers cannot create duplicate NKAs (unique PDA per researcher + specimen number)
  • Bounties cannot be created with duplicate numbers (sequential counter + unique PDA)
  • Protocol can only be initialized once (singleton PDA)

Vault Authority and Token Security

Bounty vault token accounts use the vault PDA itself as authority, ensuring only CPI transfers with correct seeds:
#[account(
    init,
    payer = creator,
    token::mint = usdc_mint,
    token::authority = vault,  // Vault is its own authority
    seeds = [b"bounty_vault", bounty.key().as_ref()],
    bump,
)]  // lib.rs:353-360
All transfers require program-signed CPI calls with correct seed derivation:
let vault_seeds: &[&[u8]] = &[
    b"bounty_vault",
    bounty_key.as_ref(),
    &[bounty.vault_bump],
];  // lib.rs:178-182, lib.rs:241-245

transfer_checked(
    CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        TransferChecked { /* ... */ },
        &[vault_seeds],  // Signer seeds
    ),
    amount,
    decimals,
)?;  // lib.rs:185-198, lib.rs:249-262
No user wallet can sign for vault transfers. Only the program can authorize token movements through CPI with valid PDA seeds.

Safe Arithmetic Operations

All fee and payout calculations use checked arithmetic to prevent overflow/underflow:
let fee = total
    .checked_mul(fee_bps)
    .ok_or(NullGraphError::FeeOverflow)?
    .checked_div(10_000)
    .ok_or(NullGraphError::FeeOverflow)?;  // lib.rs:169-173

let payout = total
    .checked_sub(fee)
    .ok_or(NullGraphError::FeeOverflow)?;  // lib.rs:174
Protected operations:
  • Fee calculation: reward_amount * fee_bps / 10_000 (lib.rs:169-173)
  • Payout calculation: reward_amount - fee (lib.rs:174)
  • Counter increments: state.nka_counter += 1 (Rust panic on overflow in debug mode)

Transfer Validation

All token transfers use transfer_checked, which validates mint address and decimal precision:
use anchor_spl::token_interface::{
    transfer_checked, TransferChecked,
};  // lib.rs:3-4

let decimals = ctx.accounts.usdc_mint.decimals;  // lib.rs:99, lib.rs:176, lib.rs:239

transfer_checked(
    CpiContext::new(/* ... */),
    reward_amount,
    decimals,  // Validates precision
)?;  // lib.rs:100-112
Using transfer_checked instead of transfer prevents token decimal confusion attacks and mint spoofing.
Locations of transfer_checked calls:
  • Bounty creation escrow: lib.rs:100-112
  • Researcher payout: lib.rs:185-198
  • Treasury fee transfer: lib.rs:202-216
  • Bounty refund: lib.rs:249-262

Input Validation

The program validates critical inputs before processing:
require!(reward_amount > 0, NullGraphError::InvalidRewardAmount);  // lib.rs:78
Additional validation through Anchor constraints:
  • PDA bump validation (automatic via bump field)
  • Account ownership validation (automatic via Account<'info, T>)
  • Program ownership validation (automatic via seeds constraint)

Submission Matching Validation

The approval instruction validates that the correct submission is being approved:
require!(
    bounty.matched_submission == ctx.accounts.submission.key(),
    NullGraphError::SubmissionMismatch
);  // lib.rs:158-161
This prevents approving unauthorized or incorrect submissions.

Frontend Security

The React frontend implements client-side security best practices:

Private Key Protection

  • No private keys are handled by the application
  • All transaction signing delegated to Phantom wallet
  • Uses Solana Wallet Adapter standard interface

Data Integrity

Client-side file hashing via Web Crypto API:
const hashBuffer = await crypto.subtle.digest('SHA-256', fileBuffer);
The SHA-256 hash is stored on-chain (lib.rs:60), providing tamper-proof data fingerprints without storing files on-chain.

Input Sanitization

All text inputs enforce length limits matching on-chain field sizes:
  • Hypothesis: 128 bytes max
  • Methodology: 128 bytes max
  • Expected/Actual Outcome: 128 bytes max each
  • Bounty Description: 256 bytes max
UTF-8 characters may consume multiple bytes. The frontend should validate byte length, not character count.

RPC Security

  • All RPC calls use HTTPS endpoints
  • No sensitive data transmitted in RPC requests
  • Read-only operations use standard getProgramAccounts
  • Write operations require wallet signature

Known Limitations

This is a hackathon/demo project. The following security considerations have NOT been addressed:

Not Implemented

  1. Formal verification - No mathematical proof of correctness
  2. Professional security audit - No third-party review
  3. Fuzzing tests - Limited edge case coverage
  4. Rate limiting - No spam protection at protocol level
  5. Upgrade authority - No mechanism for bug fixes post-deployment
  6. Access control - No admin roles beyond initial authority
  7. Emergency pause - No circuit breaker for critical bugs
  8. Time-based controls - Deadline validation not enforced in all instructions
  9. Economic attack resistance - No analysis of MEV or front-running risks
  10. Dispute resolution - Status field exists but no enforcement mechanism

Production Requirements

Before production deployment:
  1. Conduct formal security audit with reputable firm
  2. Implement comprehensive test suite with >90% coverage
  3. Add deadline validation to submit_to_bounty instruction
  4. Implement admin controls and upgrade authority
  5. Add circuit breaker for emergency situations
  6. Conduct economic security analysis
  7. Implement dispute resolution workflow
  8. Add rate limiting at RPC level
  9. Deploy to testnet for extended period
  10. Bug bounty program

Security Contact

For security concerns or vulnerability reports, please contact the development team through appropriate channels.
Never share private keys, seed phrases, or wallet credentials with anyone, including project maintainers.

Build docs developers (and LLMs) love