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.
| Field | Type | Description |
|---|
authority | Pubkey | Protocol admin wallet |
nka_counter | u64 | Auto-incrementing NKA counter |
bounty_counter | u64 | Auto-incrementing bounty counter |
fee_basis_points | u16 | Fee on settlement (250 = 2.5%) |
treasury | Pubkey | Treasury wallet for collected fees |
bump | u8 | PDA 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.
| Field | Type | Description |
|---|
researcher | Pubkey | Submitting wallet |
specimen_number | u64 | Sequential 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_value | u32 | Fixed-point (8700 = 0.8700) |
sample_size | u32 | Sample size n |
data_hash | [u8; 32] | SHA-256 of attached data file |
status | u8 | 0 = Pending, 1 = Verified, 2 = Disputed |
created_at | i64 | Unix timestamp |
bump | u8 | PDA 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.
| Field | Type | Description |
|---|
creator | Pubkey | Bounty poster wallet |
bounty_number | u64 | Sequential ID (NB-0001, NB-0002, …) |
description | [u8; 256] | Description of null result needed |
reward_amount | u64 | BIO reward in base units (6 decimals) |
BIO_mint | Pubkey | Token mint address (BIO) |
vault | Pubkey | Vault token account PDA |
deadline | i64 | Deadline as Unix timestamp |
status | u8 | 0 = Open, 1 = Matched, 2 = Fulfilled, 3 = Closed |
matched_submission | Pubkey | BountySubmission PDA (zeroed if unmatched) |
created_at | i64 | Unix timestamp |
vault_bump | u8 | Vault PDA bump |
bump | u8 | PDA 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.
4. BountySubmission (Links an NKA to a bounty)
Created when a researcher submits their NKA to a bounty.
| Field | Type | Description |
|---|
researcher | Pubkey | Claimant wallet |
null_result | Pubkey | NullResult PDA key |
bounty | Pubkey | NullBounty PDA key |
status | u8 | 0 = Pending, 1 = Approved, 2 = Rejected |
created_at | i64 | Unix timestamp |
bump | u8 | PDA 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
| Route | Page | Description |
|---|
/ | Landing | Hero, problem/solution, how-it-works timeline |
/dashboard | Dashboard | Protocol stats and full NKA registry grid |
/submit | Submit | Four-step guided form |
/market | Market | Bounty card grid with create-bounty modal |
/market/:bountyId | BountyDetail | Single bounty view with submissions |
/nka/:specimenNumber | NullResultDetail | Full 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
| Technology | Version | Purpose |
|---|
| Solana | Devnet | Settlement layer |
| Anchor | 0.31.1 | Program framework |
| anchor-spl | 0.31.1 | SPL Token Interface CPI |
| Rust | 2021 edition | Program language |
Frontend
| Technology | Version | Purpose |
|---|
| React | 19.2 | UI framework |
| TypeScript | 5.9 | Type safety |
| Vite | 7.3 | Build tool + dev server |
| Tailwind CSS | 4.2 | Utility-first styling |
| React Router | 7.13 | Client-side routing |
| Lucide React | 0.575 | Icons |
Solana client libraries
| Library | Purpose |
|---|
@coral-xyz/anchor (0.32.1) | Anchor client for IDL-based program interaction |
@solana/web3.js | JSON-RPC client and transaction building |
@solana/wallet-adapter-react | React hooks for wallet connection |
@solana/wallet-adapter-react-ui | Wallet connection modal and button |
@solana/wallet-adapter-wallets | Phantom wallet adapter |
@solana/spl-token | SPL Token client utilities |
What’s next?
Now that you understand the architecture, explore: