Skip to main content

Architecture

NullGraph is a fully on-chain protocol built on Solana with zero backend infrastructure. All state lives in Program Derived Addresses (PDAs), all logic executes in an Anchor program, and all reads happen via JSON-RPC.

System overview

                +----------------------------+
                |   Researcher's Browser     |
                |   (React 19 + Vite 7.3)   |
                +-----+--------------+-------+
                      |              |
           Write (tx) |              | Read (RPC)
                      v              v
       +--------------+--------------+-----------+
       |        Solana Devnet Cluster             |
       |                                          |
       |  +------------------------------------+  |
       |  |  NullGraph Anchor Program          |  |
       |  |  (2u3DXQq...CgZK)                 |  |
       |  |                                    |  |
       |  |  Accounts:                         |  |
       |  |   - ProtocolState (singleton)      |  |
       |  |   - NullResult PDAs                |  |
       |  |   - NullBounty PDAs                |  |
       |  |   - BountySubmission PDAs          |  |
       |  |   - Vault Token Accounts           |  |
       |  +------------------------------------+  |
       |                                          |
       |  +------------------------------------+  |
       |  |  SPL Token Interface               |  |
       |  |  (BIO transfers via CPI)           |  |
       |  +------------------------------------+  |
       +------------------------------------------+

Data flow

  • Writes go through the Solana Wallet Adapter. The user signs transactions in Phantom, which are submitted to the Anchor program.
  • Reads use getProgramAccounts RPC calls through the Anchor client. There is no backend server, database, or indexer.
  • BIO transfers use CPI transfer_checked to the SPL Token Interface, which validates the mint and decimal precision.
No off-chain infrastructure means no single point of failure, no database migrations, and no hosting costs.

On-chain program

The Anchor program lives in a single file (programs/nullgraph/src/lib.rs, ~593 lines). It uses Anchor 0.31.1 with anchor-spl for SPL Token Interface CPI and associated token accounts.

Program ID

declare_id!("2u3DXQq9A6UgMryeVSWCNdYLy3Fjh391R5hcfWYkCgZK");
Deployed to Solana Devnet at 2u3DXQq9A6UgMryeVSWCNdYLy3Fjh391R5hcfWYkCgZK.

Account architecture

NullGraph uses four main account types, all implemented as Program Derived Addresses:

1. ProtocolState (Singleton)

Global configuration storing auto-incrementing counters, fee rate, and treasury address.
FieldTypeDescription
authorityPubkeyProtocol admin wallet
nka_counteru64Auto-incrementing NKA counter
bounty_counteru64Auto-incrementing bounty counter
fee_basis_pointsu16Fee on settlement (250 = 2.5%)
treasuryPubkeyTreasury wallet for collected fees
bumpu8PDA bump seed
PDA Seeds: ["protocol_state"]
The singleton pattern ensures a single source of truth for protocol configuration and prevents counter collision.

2. NullResult (One per NKA)

Stores all metadata for a Null Knowledge Asset.
FieldTypeDescription
researcherPubkeySubmitting wallet
specimen_numberu64Sequential ID (NKA-0001, NKA-0002, …)
hypothesis[u8; 128]Hypothesis tested (UTF-8, zero-padded)
methodology[u8; 128]Methodology summary
expected_outcome[u8; 128]What was expected
actual_outcome[u8; 128]What actually happened
p_valueu32Fixed-point (8700 = 0.8700)
sample_sizeu32Sample size n
data_hash[u8; 32]SHA-256 of attached data file
statusu80 = Pending, 1 = Verified, 2 = Disputed
created_ati64Unix timestamp
bumpu8PDA bump seed
PDA Seeds: ["null_result", researcher_pubkey, specimen_number_le_bytes]
The data_hash field stores a SHA-256 fingerprint of any attached data file. The file stays off-chain; the hash provides a tamper-proof link.

3. NullBounty (One per bounty)

Represents a bounty posted by a BioDAO or researcher.
FieldTypeDescription
creatorPubkeyBounty poster wallet
bounty_numberu64Sequential ID (NB-0001, NB-0002, …)
description[u8; 256]Description of null result needed
reward_amountu64BIO reward in base units (6 decimals)
BIO_mintPubkeyToken mint address (BIO)
vaultPubkeyVault token account PDA
deadlinei64Deadline as Unix timestamp
statusu80 = Open, 1 = Matched, 2 = Fulfilled, 3 = Closed
matched_submissionPubkeyBountySubmission PDA (zeroed if unmatched)
created_ati64Unix timestamp
vault_bumpu8Vault PDA bump
bumpu8PDA bump seed
PDA Seeds: ["null_bounty", creator_pubkey, bounty_number_le_bytes]
Vault Seeds: ["bounty_vault", bounty_pda_key]
Bounty funds are escrowed immediately on creation. The vault token account uses the vault PDA itself as authority, ensuring funds can only be released via program CPI.
Created when a researcher submits their NKA to a bounty.
FieldTypeDescription
researcherPubkeyClaimant wallet
null_resultPubkeyNullResult PDA key
bountyPubkeyNullBounty PDA key
statusu80 = Pending, 1 = Approved, 2 = Rejected
created_ati64Unix timestamp
bumpu8PDA bump seed
PDA Seeds: ["bounty_submission", bounty_pda_key, null_result_pda_key]

Instructions

The program exposes six instructions:

initialize_protocol(fee_basis_points)

One-time setup. Creates the ProtocolState singleton with counters at zero and the given fee rate.
pub fn initialize_protocol(
    ctx: Context<InitializeProtocol>,
    fee_basis_points: u16,
) -> Result<()>
Emits: ProtocolInitialized { authority, fee_basis_points }

submit_null_result(...)

Mints a new NKA. Increments nka_counter, creates the NullResult PDA, and records all fields.
pub fn submit_null_result(
    ctx: Context<SubmitNullResult>,
    hypothesis: [u8; 128],
    methodology: [u8; 128],
    expected_outcome: [u8; 128],
    actual_outcome: [u8; 128],
    p_value: u32,
    sample_size: u32,
    data_hash: [u8; 32],
) -> Result<()>
Emits: NullResultSubmitted { specimen_number, researcher }

create_bounty(description, reward_amount, deadline)

Creates a NullBounty PDA + vault, transfers BIO from the creator’s token account to the vault.
pub fn create_bounty(
    ctx: Context<CreateBounty>,
    description: [u8; 256],
    reward_amount: u64,
    deadline: i64,
) -> Result<()>
Validation: Requires reward_amount > 0
Emits: BountyCreated { bounty_number, creator, reward_amount, deadline }

submit_to_bounty()

Links a researcher’s NKA to an open bounty. Creates a BountySubmission PDA and transitions the bounty to Matched.
pub fn submit_to_bounty(
    ctx: Context<SubmitToBounty>
) -> Result<()>
Security: Only the NKA’s owner can call this (enforced via has_one constraint)
Emits: BountySubmissionCreated { bounty_number, specimen_number, researcher }

approve_bounty_submission()

Bounty creator approves the submission. Transfers reward - fee BIO to the researcher and fee BIO to the treasury via vault CPI.
pub fn approve_bounty_submission(
    ctx: Context<ApproveBountySubmission>
) -> Result<()>
Fee calculation:
let fee = reward_amount
    .checked_mul(fee_basis_points as u64)
    .unwrap()
    .checked_div(10000)
    .unwrap();
let payout = reward_amount.checked_sub(fee).unwrap();
Emits: BountyFulfilled { bounty_number, specimen_number, researcher, payout, fee }

close_bounty()

Bounty creator reclaims escrowed BIO from any Open or Matched bounty. Vault balance returns to creator, bounty set to Closed.
pub fn close_bounty(
    ctx: Context<CloseBounty>
) -> Result<()>
Emits: BountyClosed { bounty_number, creator, refunded_amount }

Error codes

#[error_code]
pub enum NullGraphError {
    #[msg("Bounty is not in the expected status for this operation")]
    InvalidBountyStatus,
    #[msg("Submission is not in the expected status")]
    InvalidSubmissionStatus,
    #[msg("The submission PDA does not match the bounty's matched_submission")]
    SubmissionMismatch,
    #[msg("The bounty deadline has passed")]
    BountyExpired,
    #[msg("Reward amount must be greater than zero")]
    InvalidRewardAmount,
    #[msg("Arithmetic overflow during fee calculation")]
    FeeOverflow,
}

Frontend architecture

A React 19 SPA built with Vite 7.3 and Tailwind CSS v4. Connects directly to Solana devnet with no backend server.

Provider stack

ConnectionProvider (Solana RPC)
  -> WalletProvider (Phantom, auto-connect)
    -> WalletModalProvider
      -> ProgramProvider (Anchor Program instance)
        -> ToastProvider
          -> BrowserRouter
The ProgramProvider exposes a single Anchor Program instance constructed from the compiled IDL:
import { Program, AnchorProvider } from '@coral-xyz/anchor';
import { Nullgraph } from './lib/nullgraph_types';
import idl from './lib/nullgraph.json';

const program = new Program<Nullgraph>(
  idl as Nullgraph,
  provider
);

Routes

RoutePageDescription
/LandingHero, problem/solution, how-it-works timeline
/dashboardDashboardProtocol stats and full NKA registry grid
/submitSubmitFour-step guided form
/marketMarketBounty card grid with create-bounty modal
/market/:bountyIdBountyDetailSingle bounty view with submissions
/nka/:specimenNumberNullResultDetailFull NKA metadata with Explorer links

Data hooks

All data fetching uses custom React hooks that wrap program.account.<type>.all():
  • useProtocolState() - Fetches the singleton ProtocolState
  • useNullResults() - Fetches all NullResult accounts
  • useBounties() - Fetches all NullBounty accounts
  • useBountySubmissions(filters) - Fetches BountySubmission accounts (filterable by bounty or researcher)
All hooks expose { data, loading, error, refetch }.
No indexer required — the frontend fetches all accounts on page load. For production scale, consider using a Solana indexer like Helius or GenesysGo.

Transaction hooks

Transaction hooks derive PDAs, build instructions, send transactions, and show toast notifications:
  • useSubmitNullResult() - Mints a new NKA
  • useCreateBounty() - Creates a bounty with escrowed BIO
  • useSubmitToBounty() - Links an NKA to a bounty
  • useApproveBountySubmission() - Approves a submission and pays out
  • useCloseBounty() - Closes a bounty and refunds escrow
Example from useSubmitNullResult:
const [protocolStatePda] = PublicKey.findProgramAddressSync(
  [Buffer.from(SEEDS.PROTOCOL_STATE)],
  program.programId
);

const [nullResultPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from(SEEDS.NULL_RESULT),
    wallet.publicKey.toBuffer(),
    specimenNumberBuffer,
  ],
  program.programId
);

await program.methods
  .submitNullResult(
    hypothesisBytes,
    methodologyBytes,
    expectedOutcomeBytes,
    actualOutcomeBytes,
    pValueFixed,
    sampleSize,
    dataHashBytes
  )
  .accounts({
    researcher: wallet.publicKey,
    protocolState: protocolStatePda,
    nullResult: nullResultPda,
    systemProgram: SystemProgram.programId,
  })
  .rpc();

Design system

Dark-mode cyberpunk aesthetic defined as Tailwind v4 @theme tokens:
  • Background: #060810 with a 40px CSS grid overlay
  • Surfaces: #0d1017 (surface), #161c2a (raised), #232840 borders
  • Accents: Neon cyan (#5ec4de), magenta (#c8836a), lime (#62b862)
  • Typography: Cabinet Grotesk (display), Satoshi (body), Space Grotesk (mono/data)
  • Effects: CRT scanline animation, SVG noise overlay, glass card hover lifts with colored glow shadows

Security model

On-chain security

  • Signer checks on every write instruction via Anchor struct-level enforcement
  • PDA ownership — all data accounts are program-owned PDAs
  • has_one constraints ensure only the correct wallets can perform actions
  • Status guards validate current status before every state mutation
  • Replay protection — PDA init constraints prevent duplicate accounts
  • Vault authority — vault token accounts use the vault PDA itself as authority
  • Safe arithmetic — all fee/payout math uses checked_mul, checked_div, checked_sub
  • Transfer validation — all BIO transfers use transfer_checked, validating mint and decimal precision
Never use transfer for SPL tokens — always use transfer_checked to prevent decimal/mint confusion attacks.

Frontend security

  • No private keys handled. All signing delegated to Phantom.
  • Client-side file hashing via Web Crypto API (crypto.subtle.digest).
  • Input bounds match on-chain field sizes (128/256 chars via maxLength).

Tech stack summary

On-chain

TechnologyVersionPurpose
SolanaDevnetSettlement layer
Anchor0.31.1Program framework
anchor-spl0.31.1SPL Token Interface CPI
Rust2021 editionProgram language

Frontend

TechnologyVersionPurpose
React19.2UI framework
TypeScript5.9Type safety
Vite7.3Build tool + dev server
Tailwind CSS4.2Utility-first styling
React Router7.13Client-side routing
Lucide React0.575Icons

Solana client libraries

LibraryPurpose
@coral-xyz/anchor (0.32.1)Anchor client for IDL-based program interaction
@solana/web3.jsJSON-RPC client and transaction building
@solana/wallet-adapter-reactReact hooks for wallet connection
@solana/wallet-adapter-react-uiWallet connection modal and button
@solana/wallet-adapter-walletsPhantom wallet adapter
@solana/spl-tokenSPL Token client utilities

What’s next?

Now that you understand the architecture, explore:

Build docs developers (and LLMs) love