> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/Ge0frey/nullgraph/llms.txt
> Use this file to discover all available pages before exploring further.

# Bounty Marketplace

> Discover how BioDAOs and researchers post bounties for specific null results, with automatic escrow and settlement denominated in BIO tokens.

# Bounty Marketplace

The NullGraph Bounty Marketplace connects researchers who own Null Knowledge Assets (NKAs) with BioDAOs and research organizations willing to pay for specific negative results.

<Note>
  All bounties are **denominated in BIO tokens** and use **automatic escrow** — rewards are locked on-chain the moment a bounty is created.
</Note>

## How It Works

<Steps>
  <Step title="BioDAO Posts Bounty">
    Creator describes needed null result, sets BIO reward amount, and chooses deadline
  </Step>

  <Step title="Automatic Escrow">
    BIO tokens are immediately transferred to a program-controlled vault
  </Step>

  <Step title="Researcher Submits NKA">
    A researcher with matching NKA links their asset to the bounty
  </Step>

  <Step title="Creator Reviews">
    Bounty creator reviews submission and either approves or closes bounty
  </Step>

  <Step title="Instant Payout">
    On approval: 97.5% to researcher, 2.5% to protocol treasury
  </Step>
</Steps>

## Bounty Account Structure

Each bounty is stored in a `NullBounty` PDA:

```rust theme={null}
pub struct NullBounty {
    pub creator: Pubkey,              // Bounty poster wallet
    pub bounty_number: u64,           // Sequential ID (NB-0001, NB-0002...)
    pub description: [u8; 256],       // Description of null result needed
    pub reward_amount: u64,           // BIO reward in base units (6 decimals)
    pub usdc_mint: Pubkey,            // Token mint address (BIO)
    pub vault: Pubkey,                // Vault token account PDA
    pub deadline: i64,                // Deadline as Unix timestamp
    pub status: u8,                   // 0=Open, 1=Matched, 2=Fulfilled, 3=Closed
    pub matched_submission: Pubkey,   // BountySubmission PDA (zeroed if unmatched)
    pub created_at: i64,              // Unix timestamp
    pub vault_bump: u8,               // Vault PDA bump
    pub bump: u8,                     // PDA bump seed
}
```

<Info>
  **PDA Seeds:**

  * Bounty: `["null_bounty", creator_pubkey, bounty_number_le_bytes]`
  * Vault: `["bounty_vault", bounty_pda_key]`
</Info>

## Bounty Status Flow

Bounties transition through four states:

```mermaid theme={null}
graph LR
    A[Open] -->|researcher submits| B[Matched]
    B -->|creator approves| C[Fulfilled]
    A -->|creator closes| D[Closed]
    B -->|creator closes| D
```

| Status        | Value | Description                                  |
| ------------- | ----- | -------------------------------------------- |
| **Open**      | `0`   | Awaiting submissions, no NKA linked          |
| **Matched**   | `1`   | An NKA has been submitted, awaiting approval |
| **Fulfilled** | `2`   | Submission approved, payout complete         |
| **Closed**    | `3`   | Creator reclaimed funds, no longer active    |

## Creating a Bounty

### Instruction: `create_bounty`

```rust theme={null}
pub fn create_bounty(
    ctx: Context<CreateBounty>,
    description: [u8; 256],
    reward_amount: u64,
    deadline: i64,
) -> Result<()> {
    require!(reward_amount > 0, NullGraphError::InvalidRewardAmount);

    let state = &mut ctx.accounts.protocol_state;
    state.bounty_counter += 1;
    let bounty_number = state.bounty_counter;

    let bounty = &mut ctx.accounts.bounty;
    bounty.creator = ctx.accounts.creator.key();
    bounty.bounty_number = bounty_number;
    bounty.description = description;
    bounty.reward_amount = reward_amount;
    bounty.usdc_mint = ctx.accounts.usdc_mint.key();
    bounty.vault = ctx.accounts.vault.key();
    bounty.deadline = deadline;
    bounty.status = 0; // Open
    bounty.matched_submission = Pubkey::default();
    // ...

    // Transfer BIO from creator to vault
    transfer_checked(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            TransferChecked {
                from: ctx.accounts.creator_usdc_ata.to_account_info(),
                mint: ctx.accounts.usdc_mint.to_account_info(),
                to: ctx.accounts.vault.to_account_info(),
                authority: ctx.accounts.creator.to_account_info(),
            },
        ),
        reward_amount,
        decimals,
    )?;

    emit!(BountyCreated { /* ... */ });
    Ok(())
}
```

<Card title="Escrow Guarantee" icon="lock">
  The moment `create_bounty` succeeds, **BIO tokens leave the creator's wallet** and are locked in the vault. The creator cannot access these funds except by closing the bounty.
</Card>

### Frontend Example

```typescript theme={null}
const createBounty = async (
  program: Program,
  description: string,
  rewardAmount: number,  // in BIO (human-readable)
  deadlineDate: Date
) => {
  const wallet = useAnchorWallet();
  if (!wallet) throw new Error('Wallet not connected');

  const BIO_MINT = new PublicKey('BioTokenMintAddress...');
  const BIO_DECIMALS = 6;

  // Convert to base units
  const rewardBaseUnits = Math.floor(rewardAmount * Math.pow(10, BIO_DECIMALS));
  const deadline = Math.floor(deadlineDate.getTime() / 1000);

  // Derive PDAs
  const [protocolState] = PublicKey.findProgramAddressSync(
    [Buffer.from('protocol_state')],
    program.programId
  );

  const state = await program.account.protocolState.fetch(protocolState);
  const nextBountyNumber = state.bountyCounter.toNumber() + 1;

  const [bounty] = PublicKey.findProgramAddressSync(
    [
      Buffer.from('null_bounty'),
      wallet.publicKey.toBuffer(),
      Buffer.from(new BigUint64Array([BigInt(nextBountyNumber)]).buffer),
    ],
    program.programId
  );

  const [vault] = PublicKey.findProgramAddressSync(
    [Buffer.from('bounty_vault'), bounty.toBuffer()],
    program.programId
  );

  const creatorAta = getAssociatedTokenAddressSync(
    BIO_MINT,
    wallet.publicKey,
    false,
    TOKEN_2022_PROGRAM_ID
  );

  // Encode description
  const descBuffer = new Uint8Array(256);
  descBuffer.set(new TextEncoder().encode(description.slice(0, 256)));

  const tx = await program.methods
    .createBounty(
      Array.from(descBuffer),
      new BN(rewardBaseUnits),
      new BN(deadline)
    )
    .accounts({
      creator: wallet.publicKey,
      protocolState,
      bounty,
      vault,
      creatorUsdcAta: creatorAta,
      usdcMint: BIO_MINT,
    })
    .rpc();

  console.log(`Bounty NB-${String(nextBountyNumber).padStart(4, '0')} created`);
  console.log(`${rewardAmount} BIO escrowed in vault`);
};
```

## Submitting to a Bounty

### Instruction: `submit_to_bounty`

Researchers link their existing NKA to an open bounty:

```rust theme={null}
pub fn submit_to_bounty(ctx: Context<SubmitToBounty>) -> Result<()> {
    let bounty_status = ctx.accounts.bounty.status;
    require!(bounty_status == 0, NullGraphError::InvalidBountyStatus); // Must be Open

    let researcher_key = ctx.accounts.researcher.key();
    let null_result_key = ctx.accounts.null_result.key();
    let bounty_key = ctx.accounts.bounty.key();
    let specimen_number = ctx.accounts.null_result.specimen_number;

    let submission = &mut ctx.accounts.submission;
    submission.researcher = researcher_key;
    submission.null_result = null_result_key;
    submission.bounty = bounty_key;
    submission.status = 0; // Pending
    submission.created_at = Clock::get()?.unix_timestamp;
    submission.bump = ctx.bumps.submission;

    let submission_key = submission.key();

    let bounty = &mut ctx.accounts.bounty;
    bounty.status = 1; // Matched
    bounty.matched_submission = submission_key;

    emit!(BountySubmissionCreated { /* ... */ });
    Ok(())
}
```

<Info>
  **Security:** The `has_one = researcher` constraint in the account context ensures only the NKA owner can submit it.
</Info>

### Submission Account

Each submission creates a `BountySubmission` PDA:

```rust theme={null}
pub struct BountySubmission {
    pub researcher: Pubkey,       // Claimant wallet
    pub null_result: Pubkey,      // NullResult PDA key
    pub bounty: Pubkey,           // NullBounty PDA key
    pub status: u8,               // 0=Pending, 1=Approved, 2=Rejected
    pub created_at: i64,          // Unix timestamp
    pub bump: u8,                 // PDA bump seed
}
```

**Seeds:** `["bounty_submission", bounty_pda_key, null_result_pda_key]`

<Tip>
  Submission PDAs are **unique per bounty-NKA pair**, preventing duplicate submissions of the same NKA to the same bounty.
</Tip>

## Approving a Submission

### Instruction: `approve_bounty_submission`

Bounty creator approves the submission, triggering automatic payout:

```rust theme={null}
pub fn approve_bounty_submission(ctx: Context<ApproveBountySubmission>) -> Result<()> {
    let bounty = &mut ctx.accounts.bounty;
    require!(bounty.status == 1, NullGraphError::InvalidBountyStatus); // Must be Matched
    require!(
        bounty.matched_submission == ctx.accounts.submission.key(),
        NullGraphError::SubmissionMismatch
    );

    let submission = &mut ctx.accounts.submission;
    require!(submission.status == 0, NullGraphError::InvalidSubmissionStatus); // Must be Pending

    let protocol = &ctx.accounts.protocol_state;
    let fee_bps = protocol.fee_basis_points as u64;
    let total = bounty.reward_amount;
    
    // Calculate fee (default 2.5%)
    let fee = total
        .checked_mul(fee_bps)
        .ok_or(NullGraphError::FeeOverflow)?
        .checked_div(10_000)
        .ok_or(NullGraphError::FeeOverflow)?;
    
    let payout = total.checked_sub(fee).ok_or(NullGraphError::FeeOverflow)?;

    let decimals = ctx.accounts.usdc_mint.decimals;
    let bounty_key = bounty.key();
    let vault_seeds: &[&[u8]] = &[
        b"bounty_vault",
        bounty_key.as_ref(),
        &[bounty.vault_bump],
    ];

    // 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,
        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,
            decimals,
        )?;
    }

    bounty.status = 2; // Fulfilled
    submission.status = 1; // Approved

    emit!(BountyFulfilled {
        bounty_number: bounty.bounty_number,
        specimen_number: ctx.accounts.null_result.specimen_number,
        researcher: submission.researcher,
        payout,
        fee,
    });
    Ok(())
}
```

<CardGroup cols={2}>
  <Card title="Atomic Payout" icon="zap">
    Both transfers (researcher + treasury) happen in a **single transaction**. Either both succeed or both fail.
  </Card>

  <Card title="Vault Authority" icon="key">
    Vault uses **PDA signer seeds** to authorize transfers. No private keys involved.
  </Card>
</CardGroup>

## Closing a Bounty

### Instruction: `close_bounty`

Creators can reclaim escrowed BIO from Open or Matched bounties:

```rust theme={null}
pub fn close_bounty(ctx: Context<CloseBounty>) -> Result<()> {
    let bounty = &mut ctx.accounts.bounty;
    require!(
        bounty.status == 0 || bounty.status == 1,
        NullGraphError::InvalidBountyStatus
    );

    let vault_balance = ctx.accounts.vault.amount;
    let decimals = ctx.accounts.usdc_mint.decimals;
    let bounty_key = bounty.key();
    let vault_seeds: &[&[u8]] = &[
        b"bounty_vault",
        bounty_key.as_ref(),
        &[bounty.vault_bump],
    ];

    // Refund creator
    if vault_balance > 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.creator_usdc_ata.to_account_info(),
                    authority: ctx.accounts.vault.to_account_info(),
                },
                &[vault_seeds],
            ),
            vault_balance,
            decimals,
        )?;
    }

    bounty.status = 3; // Closed

    emit!(BountyClosed {
        bounty_number: bounty.bounty_number,
        creator: bounty.creator,
        refunded_amount: vault_balance,
    });
    Ok(())
}
```

<Note>
  **Cannot close Fulfilled bounties** — once approved, the bounty is permanently marked as Fulfilled and vault is already empty.
</Note>

## BIO Token Integration

### SPL Token Interface

All token operations use the **SPL Token Interface** (`anchor-spl/token_interface`):

```rust theme={null}
use anchor_spl::token_interface::{
    Mint, TokenAccount, TokenInterface,
    transfer_checked, TransferChecked,
};
```

### Why `transfer_checked`?

* **Validates mint** — ensures tokens are actually BIO, not a fake mint
* **Validates decimals** — prevents precision errors
* **Safer than `transfer`** — explicit validation at instruction level

```rust theme={null}
transfer_checked(
    ctx,
    amount,        // Raw base units (e.g., 1_000_000 = 1 BIO)
    decimals,      // Must match mint (6 for BIO)
)?;
```

### BIO Decimals

BIO tokens use **6 decimals**:

```
1 BIO = 1_000_000 base units
0.5 BIO = 500_000 base units
100 BIO = 100_000_000 base units
```

**Frontend conversion:**

```typescript theme={null}
const bioToBaseUnits = (bio: number) => Math.floor(bio * 1_000_000);
const baseUnitsToBio = (raw: number) => raw / 1_000_000;
```

## Events

All bounty lifecycle actions emit events for indexing:

```rust theme={null}
#[event]
pub struct BountyCreated {
    pub bounty_number: u64,
    pub creator: Pubkey,
    pub reward_amount: u64,
    pub deadline: i64,
}

#[event]
pub struct BountySubmissionCreated {
    pub bounty_number: u64,
    pub specimen_number: u64,
    pub researcher: Pubkey,
}

#[event]
pub struct BountyFulfilled {
    pub bounty_number: u64,
    pub specimen_number: u64,
    pub researcher: Pubkey,
    pub payout: u64,
    pub fee: u64,
}

#[event]
pub struct BountyClosed {
    pub bounty_number: u64,
    pub creator: Pubkey,
    pub refunded_amount: u64,
}
```

## Security Model

<CardGroup cols={2}>
  <Card title="Signer Checks" icon="signature">
    Only `creator` can approve/close their bounty (enforced via `has_one = creator`)
  </Card>

  <Card title="Status Guards" icon="shield-check">
    Each instruction validates current status before mutation
  </Card>

  <Card title="Submission Match" icon="link">
    `approve_bounty_submission` verifies `matched_submission` PDA matches provided submission
  </Card>

  <Card title="Safe Math" icon="calculator">
    All fee/payout calculations use `checked_mul`, `checked_div`, `checked_sub`
  </Card>
</CardGroup>

## Error Handling

```rust theme={null}
#[error_code]
pub enum NullGraphError {
    #[msg("Bounty is not in the expected status")]
    InvalidBountyStatus,
    #[msg("Submission is not in the expected status")]
    InvalidSubmissionStatus,
    #[msg("Matched submission mismatch")]
    SubmissionMismatch,
    #[msg("Reward amount must be > 0")]
    InvalidRewardAmount,
    #[msg("Fee calculation overflow")]
    FeeOverflow,
}
```

## Browse Bounties

Fetch all bounties from the program:

```typescript theme={null}
const bounties = await program.account.nullBounty.all();

const openBounties = bounties.filter(b => b.account.status === 0);
const fulfilledBounties = bounties.filter(b => b.account.status === 2);

console.log(`Open: ${openBounties.length}`);
console.log(`Fulfilled: ${fulfilledBounties.length}`);
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Protocol Fees" href="/features/protocol-fees" icon="percent">
    Deep dive into the 2.5% fee model and treasury distribution
  </Card>

  <Card title="BIO Integration" href="/features/bio-integration" icon="coins">
    Learn about Bio Protocol alignment and token economics
  </Card>
</CardGroup>
