Program: programs/stokkie/src/lib.rs
Last audited: March 2026 (Phase 14)
Status: Pre-mainnet, all findings addressed
| 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) |
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.
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.
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.
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.
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.
Observation: Deposits blocked while paused, but withdrawals allowed. Decision: Intentional — emergency exit path. Documented in Phase 5.
Observation: amount * bps / 10_000 floors to 0 for small amounts.
Decision: Standard BPS rounding. Dust amounts are negligible (<1 lamport per cycle).
- Authority — stokkie creator, has admin powers (pause, yield, toggle enrollment)
- Member — can deposit, vote, propose, accept invites
- Non-member — can only accept invite (if enrollment open)
- Malicious program — CPI re-entrancy vector
| 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 |
members.len() == contributions.len() == member_contribution_count.len()payout_scheduleis a subset ofmemberspayout_index < payout_schedule.len()(enforced by modulo)- Vault balance >= sum of all member claims (no fractional reserve)
- Only Active stokkies accept deposits and yield operations
- Paused stokkies allow close + kamino_withdraw (emergency exit)
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 | 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.