Skip to content

Security: nderman/stokkie

Security

docs/SECURITY.md

Security Audit Report — Stokkie Program

Program: programs/stokkie/src/lib.rs Last audited: March 2026 (Phase 14) Status: Pre-mainnet, all findings addressed


Instruction Access Control Matrix

Instruction Who Can Call Enforced By
initialize_stokkie Anyone (becomes authority) payer: Signer
deposit_contribution Members only member: Signer + members.contains()
process_rotation Authority or Treasurer caller: Signer + require_role_or_authority
create_proposal Members only creator: Signer + members.contains()
cast_vote Members only voter: Signer + members.contains()
execute_proposal Anyone (quorum enforced) votes_for >= floor(N/2)+1
accept_invite Anyone (enrollment checks) new_member: Signer + open_enrollment + !GroupFull + !DuplicateMember
toggle_enrollment Authority or Chair caller: Signer + require_role_or_authority
pause_stokkie Authority only authority: Signer + has_one = authority
unpause_stokkie Authority only authority: Signer + has_one = authority
close_stokkie Quorum of members ensure_member_votes (majority sign)
kamino_deposit Authority only authority: Signer + has_one = authority
kamino_withdraw Authority only authority: Signer + has_one = authority
bid_payout_slot Members only bidder: Signer + members.contains()
vote_update_schedule Quorum of members ensure_member_votes (majority sign)
update_config Quorum of members ensure_member_votes (majority sign)

Findings & Resolutions

CRITICAL-1: Division by Zero in close_stokkie (FIXED)

Risk: If all members removed via proposals, close_stokkie divides by member_count = 0. Fix: Added require!(member_count > 0, TooFewMembers) guard before division. Note: Practically unreachable — ensure_member_votes requires majority to sign, impossible with 0 members. Guard added as defense-in-depth.

MEDIUM-1: payout_index Reset on Member Removal (FIXED)

Risk: Removing a member reset payout_index = 0, disrupting rotation order. Fix: Smart adjustment — if removed member was before current index, decrement; wrap index to new schedule length.

MEDIUM-2: payout_index u8 Overflow at 256 Cycles (FIXED)

Risk: payout_index: u8 wraps at 256. With 100 members, only ~2.5 full rotations before corruption. Fix: Changed to u16 (max 65,535 cycles). Account layout change — requires fresh deploy.

HIGH-2: Inconsistent Threshold Types (FIXED)

Risk: execute_proposal cast members.len() to u32 while ensure_member_votes used usize. Functionally equivalent but confusing. Fix: Both now use usize consistently. Comparison casts votes_for as usize.

INFO: Optional Oracle in process_rotation (ACCEPTED)

Observation: Pyth oracle peg check is opt-in — rotation proceeds without oracle if no account provided. Decision: Accepted as design choice. Oracle is defense-in-depth; mandatory oracle would break rotations if Pyth is down.

INFO: kamino_withdraw Allowed While Paused (BY DESIGN)

Observation: Deposits blocked while paused, but withdrawals allowed. Decision: Intentional — emergency exit path. Documented in Phase 5.

INFO: BPS Truncation Dust (ACCEPTED)

Observation: amount * bps / 10_000 floors to 0 for small amounts. Decision: Standard BPS rounding. Dust amounts are negligible (<1 lamport per cycle).


Threat Model

Actors

  1. Authority — stokkie creator, has admin powers (pause, yield, toggle enrollment)
  2. Member — can deposit, vote, propose, accept invites
  3. Non-member — can only accept invite (if enrollment open)
  4. Malicious program — CPI re-entrancy vector

Attack Surfaces

Vector Mitigation
Unauthorized fund access PDA vault — only invoke_signed with correct seeds can move funds
Fake token accounts in close_stokkie Each remaining_accounts[i] validated: owner == members[i], mint == stokkie.mint, owner == token_program
Re-entrancy via CPI All CPIs are to SPL Token (stateless) or Kamino (separate program). Stokkie state is mutated before CPI.
Duplicate member injection DuplicateMember check in initialize_stokkie and accept_invite
Proposal replay executed flag prevents re-execution; deadline prevents stale proposals
Double voting voters Vec tracks all voters; AlreadyVoted error on duplicate
Grief via spam proposals Proposals require SOL for account rent; no free proposal creation
Oracle manipulation Pyth feed ID hardcoded; price staleness checked (max age); +-1% tolerance
Vault drain via yield kamino_deposit capped by vault balance; authority-only gated

Invariants

  1. members.len() == contributions.len() == member_contribution_count.len()
  2. payout_schedule is a subset of members
  3. payout_index < payout_schedule.len() (enforced by modulo)
  4. Vault balance >= sum of all member claims (no fractional reserve)
  5. Only Active stokkies accept deposits and yield operations
  6. Paused stokkies allow close + kamino_withdraw (emergency exit)

Arithmetic Safety

All financial arithmetic uses saturating_* operations:

  • saturating_add, saturating_sub, saturating_mul
  • Division uses standard / (safe — divisors validated > 0)
  • BPS: amount.saturating_mul(bps as u64) / 10_000

No unchecked arithmetic in financial paths.


PDA Seed Verification

PDA Seeds Verified
Stokkie ["stokkie", authority, group_seed] init constraint in InitializeStokkie
Vault ["vault", stokkie_pda] init constraint in InitializeStokkie
Proposal ["proposal", stokkie_pda, proposal_id_le8] init constraint in CreateProposal
Branding ["branding", stokkie_pda] init constraint in SetBranding

All invoke_signed calls use stokkie_seeds() helper with correct bump.

There aren't any published security advisories