Skip to main content

Protocol Fees

NullGraph uses a configurable fee mechanism to sustain protocol development, incentivize participation, and build a treasury for future ecosystem growth.
The default fee is 2.5% (250 basis points) deducted from every bounty settlement. Fees are routed to a protocol-controlled treasury wallet.

Fee Model Overview

When Fees Apply

Fees are only charged on successful bounty settlements via the approve_bounty_submission instruction.

NKA Submission

No fee — researchers submit null results for free

Bounty Creation

No fee — creating bounties is free (only escrow cost)

Bounty Approval

2.5% fee — deducted from reward on settlement

Fee Calculation

Fees are calculated using basis points (1 bp = 0.01%):
let fee_bps = protocol.fee_basis_points as u64;  // 250 = 2.5%
let total = bounty.reward_amount;

let fee = total
    .checked_mul(fee_bps)       // total * 250
    .ok_or(NullGraphError::FeeOverflow)?
    .checked_div(10_000)        // ÷ 10,000 = 2.5%
    .ok_or(NullGraphError::FeeOverflow)?;

let payout = total.checked_sub(fee).ok_or(NullGraphError::FeeOverflow)?;
Example:
Bounty Reward: 100 BIO (100_000_000 base units)
Fee (2.5%):      2.5 BIO (2_500_000 base units)
Researcher Gets: 97.5 BIO (97_500_000 base units)
All arithmetic uses checked operations (checked_mul, checked_div, checked_sub) to prevent overflow and underflow attacks.

Protocol State Configuration

Fees are stored in the global ProtocolState singleton:
pub struct ProtocolState {
    pub authority: Pubkey,        // Protocol admin wallet
    pub nka_counter: u64,         // Auto-incrementing NKA counter
    pub bounty_counter: u64,      // Auto-incrementing bounty counter
    pub fee_basis_points: u16,    // Fee on settlement (250 = 2.5%)
    pub treasury: Pubkey,         // Treasury wallet for collected fees
    pub bump: u8,                 // PDA bump seed
}
Seeds: ["protocol_state"]

Initialization

The protocol is initialized once via initialize_protocol:
pub fn initialize_protocol(
    ctx: Context<InitializeProtocol>,
    fee_basis_points: u16,
) -> Result<()> {
    let state = &mut ctx.accounts.protocol_state;
    state.authority = ctx.accounts.authority.key();
    state.nka_counter = 0;
    state.bounty_counter = 0;
    state.fee_basis_points = fee_basis_points;  // Set fee rate
    state.treasury = ctx.accounts.treasury.key();  // Set treasury wallet
    state.bump = ctx.bumps.protocol_state;

    emit!(ProtocolInitialized {
        authority: state.authority,
        fee_basis_points,
    });
    Ok(())
}
One-time setup script:
const initProtocol = async (
  program: Program,
  authority: Keypair,
  treasuryAddress: PublicKey
) => {
  const [protocolState] = PublicKey.findProgramAddressSync(
    [Buffer.from('protocol_state')],
    program.programId
  );

  const tx = await program.methods
    .initializeProtocol(
      250  // 2.5% fee
    )
    .accounts({
      authority: authority.publicKey,
      protocolState,
      treasury: treasuryAddress,
    })
    .signers([authority])
    .rpc();

  console.log('Protocol initialized with 2.5% fee');
  console.log(`Treasury: ${treasuryAddress.toBase58()}`);
};
The fee rate is configurable at initialization but cannot be changed afterward in the current implementation. Future versions may add governance-controlled fee updates.

Treasury Distribution

Fee Collection Flow

When a bounty is approved, the vault transfers fees to the treasury in the same transaction:
// Pay researcher (97.5%)
transfer_checked(
    CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        TransferChecked {
            from: ctx.accounts.vault.to_account_info(),
            mint: ctx.accounts.usdc_mint.to_account_info(),
            to: ctx.accounts.researcher_usdc_ata.to_account_info(),
            authority: ctx.accounts.vault.to_account_info(),
        },
        &[vault_seeds],
    ),
    payout,  // 97.5%
    decimals,
)?;

// Pay treasury fee (2.5%)
if fee > 0 {
    transfer_checked(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            TransferChecked {
                from: ctx.accounts.vault.to_account_info(),
                mint: ctx.accounts.usdc_mint.to_account_info(),
                to: ctx.accounts.treasury_usdc_ata.to_account_info(),
                authority: ctx.accounts.vault.to_account_info(),
            },
            &[vault_seeds],
        ),
        fee,  // 2.5%
        decimals,
    )?;
}

Atomic Settlement

Both transfers happen in one transaction — researcher payout and treasury fee are inseparable. Either both succeed or both fail.

Treasury Account Derivation

The treasury receives BIO via its Associated Token Account (ATA):
import { getAssociatedTokenAddressSync } from '@solana/spl-token';

const treasuryWallet = new PublicKey('TreasuryWalletAddress...');
const BIO_MINT = new PublicKey('BioTokenMintAddress...');

const treasuryAta = getAssociatedTokenAddressSync(
  BIO_MINT,
  treasuryWallet,
  false,  // allowOwnerOffCurve
  TOKEN_2022_PROGRAM_ID
);

// This ATA receives all protocol fees

Fee Breakdown Example

Let’s trace a 1000 BIO bounty settlement:
StepAmount (BIO)Base UnitsRecipient
Bounty Reward1000.001_000_000_000Vault (escrowed)
Fee (2.5%)25.0025_000_000Treasury
Researcher Payout975.00975_000_000Researcher
Vault Balance After0.000
On-chain event:
emit!(BountyFulfilled {
    bounty_number: 42,
    specimen_number: 17,
    researcher: researcher_pubkey,
    payout: 975_000_000,  // 975 BIO
    fee: 25_000_000,      // 25 BIO
});

Fee Basis Points Explained

Basis PointsPercentageCalculation
00%0 / 10_000 = 0
1001%100 / 10_000 = 0.01
2502.5%250 / 10_000 = 0.025
5005%500 / 10_000 = 0.05
100010%1_000 / 10_000 = 0.10
Basis points provide precision for fractional percentages (like 2.5%) using only integer arithmetic — critical for on-chain programs.

Why 2.5%?

NullGraph’s fee model balances sustainability with accessibility:

Lower than DeFi

Most DeFi protocols charge 3-5% fees. NullGraph’s 2.5% is competitive for a scientific data marketplace.

No Creator Fee

Unlike NFT marketplaces, bounty creators pay zero fees — only settlement is taxed, incentivizing bounty creation.

Researcher-Friendly

97.5% payout ensures researchers capture most of the value they create.

Sustainable Treasury

Fees accumulate in BIO, building a war chest for grants, development, and ecosystem growth.

Treasury Use Cases

Fees collected in the treasury can fund:
  • Protocol Development — smart contract upgrades, frontend improvements
  • Community Grants — funding for researchers and BioDAOs
  • Verification Incentives — rewards for NKA peer review and validation
  • Marketing & Growth — ecosystem expansion, conference sponsorships
  • Liquidity Mining — future token incentives for participation
Treasury governance mechanisms are not yet implemented. The current treasury wallet is controlled by the protocol authority. Future versions may introduce DAO governance.

Fee Events & Tracking

Every settlement emits a BountyFulfilled event with exact fee amounts:
#[event]
pub struct BountyFulfilled {
    pub bounty_number: u64,
    pub specimen_number: u64,
    pub researcher: Pubkey,
    pub payout: u64,     // Amount sent to researcher
    pub fee: u64,        // Amount sent to treasury
}
Indexing example:
const connection = new Connection(RPC_URL);
const program = new Program(IDL, PROGRAM_ID, { connection });

program.addEventListener('BountyFulfilled', (event) => {
  const payout = event.payout.toNumber() / 1_000_000;  // BIO
  const fee = event.fee.toNumber() / 1_000_000;        // BIO
  const total = payout + fee;

  console.log(`Bounty NB-${event.bountyNumber} settled:`);
  console.log(`  Total: ${total} BIO`);
  console.log(`  Researcher: ${payout} BIO (${(payout/total*100).toFixed(2)}%)`);
  console.log(`  Treasury: ${fee} BIO (${(fee/total*100).toFixed(2)}%)`);
});

Security: Safe Math

All fee calculations use checked arithmetic to prevent exploits:
// SAFE: Uses checked_mul and checked_div
let fee = total
    .checked_mul(fee_bps)
    .ok_or(NullGraphError::FeeOverflow)?
    .checked_div(10_000)
    .ok_or(NullGraphError::FeeOverflow)?;

// SAFE: Uses checked_sub
let payout = total
    .checked_sub(fee)
    .ok_or(NullGraphError::FeeOverflow)?;
Why this matters:

Overflow Protection

Prevents attackers from causing integer overflow with massive reward amounts

Underflow Protection

Ensures payout = total - fee never underflows (e.g., if fee > total)

Deterministic Failure

Returns clear FeeOverflow error instead of silent corruption

Auditable

Explicit error handling makes fee logic easy to audit

Frontend Fee Display

Show fee breakdown before settlement:
const FeeSummary = ({ rewardAmount }: { rewardAmount: number }) => {
  const FEE_BPS = 250;  // 2.5%
  const fee = (rewardAmount * FEE_BPS) / 10_000;
  const payout = rewardAmount - fee;

  return (
    <div className="fee-summary">
      <div className="line">
        <span>Bounty Reward:</span>
        <span>{rewardAmount.toFixed(2)} BIO</span>
      </div>
      <div className="line fee">
        <span>Protocol Fee (2.5%):</span>
        <span>-{fee.toFixed(2)} BIO</span>
      </div>
      <div className="line total">
        <span>You Receive:</span>
        <span>{payout.toFixed(2)} BIO</span>
      </div>
    </div>
  );
};

Querying Treasury Balance

Check total fees collected:
const getTreasuryBalance = async (
  connection: Connection,
  treasuryWallet: PublicKey,
  bioMint: PublicKey
) => {
  const treasuryAta = getAssociatedTokenAddressSync(
    bioMint,
    treasuryWallet,
    false,
    TOKEN_2022_PROGRAM_ID
  );

  const account = await connection.getTokenAccountBalance(treasuryAta);
  const balance = parseFloat(account.value.uiAmount || '0');

  console.log(`Treasury Balance: ${balance} BIO`);
  return balance;
};

No Fees on Closed Bounties

When a bounty is closed (not approved), the full vault balance returns to the creator:
pub fn close_bounty(ctx: Context<CloseBounty>) -> Result<()> {
    // ...
    let vault_balance = ctx.accounts.vault.amount;

    // Refund creator (100% — no fee)
    if vault_balance > 0 {
        transfer_checked(
            // ... transfer full vault_balance to creator ...
        )?;
    }

    emit!(BountyClosed {
        refunded_amount: vault_balance,  // Full amount
    });
    Ok(())
}
Closing a bounty incurs zero fees — creators get 100% of their escrowed BIO back. Fees only apply on successful settlements.

Fee Rate Immutability

The current implementation sets the fee rate once at initialization:
  • Transparent — fee rate is public, stored on-chain
  • Predictable — users know exact fees before transacting
  • Not governable — cannot be changed without program upgrade
Future governance: A future version may add:
pub fn update_fee_rate(
    ctx: Context<UpdateFeeRate>,
    new_fee_bps: u16,
) -> Result<()> {
    // Require DAO governance vote or multisig authority
    let state = &mut ctx.accounts.protocol_state;
    state.fee_basis_points = new_fee_bps;
    emit!(FeeRateUpdated { new_fee_bps });
    Ok(())
}

Comparison with Other Protocols

ProtocolFee ModelFee Rate
NullGraphSettlement fee2.5%
Uniswap V3Swap fee0.05-1% (per swap)
OpenSeaMarketplace fee2.5%
Magic EdenMarketplace fee2%
TensorMarketplace fee1.5% (dynamic)
AaveBorrow feeVariable (0-8%)
NullGraph’s 2.5% is competitive with Web3 marketplace standards and lower than most DeFi lending protocols.

Next Steps

BIO Integration

Learn how BIO tokens power the bounty economy

Bounty Marketplace

Explore the full bounty lifecycle and escrow mechanics

Build docs developers (and LLMs) love