Double your BUX! Play Now →
Security · Audit report

Blockster Bankroll — security audit.

An independent, line-by-line review of the Anchor program that backs Blockster's coin flip game and dual-vault liquidity pools. 22 instructions, 7 account structs, 32 error codes, roughly 1,200 lines of Rust.

Audit · Final report
Blockster Bankroll v1
Program 49up2uzZANpjTC3sgggbZazdHBii2vY9mVK3vk5dT2tm
Report version: 1.0
Delivered: 2026-04-21
Commit: feat/solana-migration @ 1d4985d
Critical
0
High
0
Medium
2
Low
6
Informational
8
Verdict: the program is well-structured and defends against most conventional attack vectors — nonce reuse, commit-reveal tampering, SOL vault authority hijacks, PDA seed confusion, arithmetic overflow, unauthorized settlement. We surface two Medium findings related to centralization (the settler holds unilateral outcome authority and some admin keys are not on-chain rotatable) that constrain how much trust LPs can extend without additional operational controls.
01

Executive summary

This report details an independent security review of the Blockster Bankroll Anchor program, the on-chain smart contract that custodies SOL and BUX deposits, tracks bettor positions, and executes settlement for the Coin Flip game on Solana. Review was conducted at commit 1d4985d on the feat/solana-migration branch.

The program implements a commit-reveal randomness scheme, a two-vault LP pool with standard AMM-style share math, a registry of parametric games bounded by safety caps, and a two-tier referral programme. Arithmetic uses u64 with u128 intermediates and consistently employs checked_* variants. PDAs are derived with canonical seeds and bumps are stored to avoid recomputation. SPL Token and System Program CPIs use signer seeds correctly.

Two Medium findings stand out. M-01 notes that the on-chain program does not recompute win/loss from the revealed seed — it accepts the won and payout parameters from the settler at face value. Combined with the fact that the settler is a single keypair rather than a multi-signer setup, this concentrates outcome authority off-chain. M-02 flags that the program-internal authority field is not rotatable on-chain, so an authority-key compromise requires full program redeployment via the upgrade authority.

The six Low findings and eight Informational findings are mostly hardening opportunities: per-bet vs aggregate liability controls, slippage parameters on deposits, dead fields, misleading error names, and explicit documentation of where trust still resides. None are exploitable against user funds as currently configured.

The program is suitable for devnet operation as-is and is suitable for mainnet operation with the operational controls described in M-01 and M-02 (multisig on authority, documented settler rotation, published outcome-verification tooling). LPs evaluating the protocol should read M-01 carefully and form their own view on the settler trust assumption before depositing meaningful capital.

02

Scope

In scope
Crate
contracts/blockster-bankroll/programs/blockster-bankroll
lib.rs
Program entrypoints (22 instruction handlers)
instructions/
16 instruction files covering init, LP, bet lifecycle, referrals, admin
state/
7 account structs: GameRegistry, SolVaultState, BuxVaultState, PlayerState, BetOrder, ReferralState, GameEntry (embedded)
math.rs
LP price, LP mint/burn, max-bet, max-payout, referral reward calculators
errors.rs
32 error codes
events.rs
14 emitted events
CPIs
System Program transfers, SPL Token mint/burn/transfer, Metaplex Token Metadata creation
Out of scope
Settler service
contracts/blockster-settler (TypeScript) — reviewed informally for CPIs but not audited independently
Phoenix backend
Off-chain game server, Mnesia cache, LiveView UI
Deployment process
CI/CD, keypair custody, key rotation operations
Airdrop program
wxiuLBuqxem5... — separate crate not reviewed here
Legacy EVM contracts
contracts/legacy-evm preserved for archive only
Economic design
House edge calibration, game theory, expected LP returns
03

Methodology

Review was conducted as a full manual read of all Rust source files with cross-reference to the Anchor IDL. For each instruction we enumerated:

  • Signer set: which accounts are Signer<T>, and what constraints bind them.
  • Account validation: seed derivations, has_one relationships, ownership checks.
  • Pre-conditions: every require! and constraint, mapped to error codes.
  • State transitions: what fields are read and written, whether mutations can be reverted if downstream logic fails.
  • CPI safety: whether invoked_signed seeds match the PDA holding authority, and whether token programs are the ones expected.
  • Arithmetic: overflow, underflow, division-by-zero, signed-vs-unsigned, precision loss in truncation.

We built the following mental threat model and probed each instruction against it:

  • A malicious player attempting to withdraw more than their share, settle bets they didn't place, or manipulate outcomes.
  • A malicious settler attempting to drain the vault via selective settlements, reuse commitments, or pay themselves.
  • A malicious authority attempting to set exploitative config (huge multipliers, 100% fees) before a legitimate bet settles.
  • A front-runner attempting to sandwich deposits or withdrawals.
  • An uninvolved party attempting to block or poison settlement by interacting with the PDAs.

Where a threat was successfully defended, we recorded it under Verified properties. Where a threat revealed an exposure, we recorded it as a finding.

04

Findings summary

ID Title Severity Status
M-01 Outcome (won, payout) determined off-chain; settler has unilateral control Medium Acknowledged
M-02 authority field is not rotatable on-chain Medium Acknowledged
L-01 No aggregate cap on total_liability as a fraction of vault balance Low Acknowledged
L-02 Settler keypair is a single point of failure for rent recovery across placement rotations Low By design
L-03 Game multipliers can be rewritten with no slippage protection for in-flight bet prep Low Acknowledged
L-04 fee_bps is stored and validated but never applied in settlement Low Acknowledged
L-05 InvalidServerSeed error reused for has_one = player mismatches Low Acknowledged
L-06 Settler can self-DoS by spamming submit_commitment for arbitrary wallets Low By design
I-01 has_active_order field in PlayerState is dead code Informational Acknowledged
I-02 MINIMUM_LIQUIDITY is a flat 10,000 regardless of token decimals Informational By design
I-03 No slippage parameter on deposit/withdraw instructions Informational Acknowledged
I-04 submit_commitment allows arbitrarily large future nonces Informational By design
I-05 Referral rewards are paid from vault (LP cost), not from a separate fee bucket Informational By design
I-06 Pause does not block settlement — intentional but should be explicit to LPs Informational By design
I-07 UnauthorizedSettler error is raised for authority-role mismatches in pause/update_config Informational Acknowledged
I-08 Events lack bet_order pubkey, requiring indexers to derive it Informational Acknowledged
05

Medium findings

M-01

Outcome (won, payout) determined off-chain; settler has unilateral control

MEDIUM ACKNOWLEDGED
Description

The settle_bet instruction takes four caller-supplied arguments: nonce, server_seed, won, and payout. The program verifies three things:

  • SHA256(server_seed) == bet_order.commitment_hash
  • the bet has not expired
  • payout ≤ bet_order.max_payout

Critically, the program does not verify that the submitted won and payout are consistent with the revealed server seed and the bet parameters. The settler is trusted to derive them honestly off-chain.

Because the settler is a single Solana keypair (game_registry.settler) rather than a multisig, compromise of that key grants an attacker unilateral authority over outcomes. The per-bet guard payout ≤ max_payout limits exfiltration per bet to at most 2× the wager, but systematic abuse (e.g. declaring every bet a win) could drain the vault over many bets.

Impact

Scenario 1: Settler key compromise. Attacker can route losing bets to themselves as wins (at up to 2× amount) and redirect payouts to a controlled wallet via referral misdirection. Bounded per-bet but unbounded in aggregate.

Scenario 2: Settler-player collusion. Operator can collude with a specific wallet, marking every bet from that wallet as a win at max_payout. Because the off-chain RNG is private, this is undetectable to external observers absent the losing verification that M-01's remediation would enable.

Scenario 3: Buggy settler. A bug in the off-chain outcome derivation (e.g. an off-by-one in byte extraction) would systematically misreport outcomes, with no on-chain tripwire.

Scope. Affects LP solvency in the long run. Individual players are not at risk — if anything, a buggy/malicious settler has incentive to overpay, not underpay, since clients can prove underpayment off-chain and reputational damage is a credible deterrent.

Recommendation

Primary remediation (v2). Move outcome derivation on-chain. Store the bet's difficulty and predictions (max 5 bytes as a packed u8) in BetOrder. On settle, the program should:

  1. Recompute the client seed from SHA256(player_pubkey ++ amount ++ vault_type ++ difficulty ++ predictions).
  2. Compute combined = SHA256(server_seed ++ client_seed ++ nonce).
  3. Derive flips from the first N bytes, compute win per mode.
  4. Compute payout as amount × multiplier / 10000 if won, else 0.
  5. Reject the settlement if the caller's submitted won/payout disagree with the derived values. (Or drop those arguments entirely and have the program compute them authoritatively.)

This eliminates the settler's outcome authority without changing the user experience. The settler remains responsible for revealing the seed and paying the compute; the chain becomes responsible for determining the result.

Interim remediation. Migrate the settler keypair to a Squads-style multisig, publish the outcome-derivation algorithm in documentation (done: see Provably Fair page), and provide a first-class verification tool so anyone can audit a posteriori.

References
settle_bet.rs:120-161 (entry handler), settle_bet.rs:131-135 (hash check), bet_order.rs (fields that would need extending), math.rs:calculate_max_payout
M-02

authority field is not rotatable on-chain

MEDIUM ACKNOWLEDGED
Description

The authority field of GameRegistry is written exactly once — at initialize_registry — and there is no instruction to update it. The authority can rotate the settler via update_config, but cannot rotate itself.

If the authority key is compromised, the only path to rotation is redeploying the program under a new authority via the off-chain upgrade authority (a separate key). That requires (a) the upgrade authority being held by someone other than the attacker, and (b) operational readiness to re-initialize PDAs in a way that's compatible with existing state.

Impact

A compromised authority can:

  • Register a new game with arbitrary multipliers (e.g., 100× with unchecked max_bet_bps up to 5%), converting LP capital to house losses one bet at a time.
  • Rotate the settler to an attacker-controlled address and collect all rent / gain arbitrary outcome control per M-01.
  • Pause the program and leave it paused, denying users withdrawals (settlements still work, but deposits/withdrawals do not).

Caps on per-game max_bet_bps (500 = 5%) and fee_bps (1000 = 10%) bound the rate of exfiltration. The minimum multiplier of 100 (1×) prevents immediate zero-payout games, though nothing prevents the authority from registering a 1.01× game that is unprofitable for LPs over time.

Recommendation

Preferred (v2). Add a two-step transfer_authority instruction with:

  1. propose_authority(new: Pubkey) — writes pending_authority and propose_timestamp.
  2. accept_authority() — callable only by pending_authority, and only after a timelock of (say) 24h. Clears pending_authority and overwrites authority.

The timelock gives observers time to react if a proposed transfer looks suspicious. The two-step prevents accidental loss of authority to a key that doesn't exist.

Interim. Hold the authority key in a 2-of-3 Squads multisig, document the key custody clearly, and publish an incident-response runbook for upgrade-authority-assisted rotation.

References
state/game_registry.rs:52, instructions/initialize.rs (sets authority), instructions/update_config.rs (can change settler but not authority), instructions/pause.rs:10 (constraint reads authority)
06

Low findings

L-01

No aggregate cap on total_liability as a fraction of vault balance

LOW ACKNOWLEDGED
Description

Each bet is bounded in isolation by potential_profit ≤ net_balance, where net_balance = vault − rent − total_liability. As total_liability grows, net_balance shrinks, which tightens subsequent bets — but there is no hard upper bound on what fraction of the vault can be booked as liability at once.

In an extreme scenario with many simultaneous bets placed under max_bet_bps = 5% and the 31.68× difficulty, total_liability could asymptotically approach the vault balance, leaving little or no available liquidity for withdrawals until bets settle.

Impact

Not a solvency risk: the vault can always cover any single bet's max_payout because that was the placement invariant. It is a liquidity risk: withdrawals would revert with InsufficientLiquidity until pending bets resolve. In practice bets settle in seconds so queueing is rare, but a coordinated wave of bets could transiently lock LP capital.

Recommendation

Introduce a system-level constant (e.g. MAX_LIABILITY_BPS = 5000) and enforce total_liability ≤ vault_balance × MAX_LIABILITY_BPS / 10000 at placement. New bets revert with a new LiabilityCapExceeded error rather than BetTooLarge.

References
place_bet_sol.rs:104-122, place_bet_bux.rs (mirror)
L-02

Settler keypair is a single point of failure for rent recovery across placement rotations

LOW BY DESIGN
Description

Each BetOrder stores the rent_payer at placement time (enforced to equal the current settler). On settle_bet / reclaim_expired, the account closes and rent returns to that stored rent_payer via has_one.

If the settler is rotated via update_config while bets are pending, those pending bets still reference the old settler. The old key must remain accessible to:

  • Sign settle_bet (settler is the signer).
  • Receive the rent rebate on BetOrder close (both settle and reclaim).

If the old key is destroyed (e.g. compromise-forced rotation), the in-flight bets can only be resolved via reclaim_expired (player-signed), and the rent will still flow to the old settler's pubkey — which may now be attacker-controlled.

Impact

Operational inconvenience rather than a fund-loss risk for users. The rent amount per BetOrder is small (around 2,000,000 lamports / 0.002 SOL). Total at-risk rent at any moment is bounded by unsettled_count × rent_per_bet, typically under 0.1 SOL.

Recommendation

Document the rotation runbook: before rotating settler, wait for all pending bets to settle or expire. For compromise-forced rotation, accept the transient rent loss. Alternatively, relax the has_one = rent_payer constraint to allow either the stored rent_payer or the current settler to receive rent.

References
bet_order.rs:48, place_bet_sol.rs:75-78 (sets rent_payer), settle_bet.rs:62-63 (has_one), reclaim_expired.rs:53-54 (has_one)
L-03

Game multipliers can be rewritten with no slippage protection for in-flight bet prep

LOW ACKNOWLEDGED
Description

update_config allows the authority to rewrite new_game_multipliers: [u16; 9] at any time. Each multiplier must be >=100 if non-zero but is otherwise unbounded.

Placed bets are unaffected (their max_payout is fixed at placement). New bets use the new multipliers at placement time. If the UI quotes a bet at multiplier X, the user signs, and the authority rewrites to Y in between, the bet actually placed uses Y with no warning to the user.

Impact

A user can end up betting on less-favourable terms than they agreed to. Impact is small because the signing window is typically 1–2 seconds; coordinated exploitation would require the authority to front-run user signatures. Most adversarial in a compromised-authority scenario (see M-02).

Recommendation

Add an optional expected_multiplier_bps: Option<u64> argument to place_bet_sol/place_bet_bux. When Some, the program verifies it matches the current multiplier for the requested difficulty and reverts otherwise. Have the UI populate it from the live quote.

Alternatively, add a timelock to multiplier changes: writes take effect N seconds after they are proposed, giving clients time to re-quote.

References
update_config.rs:110-118 (multipliers update path), place_bet_sol.rs:100-101 (reads multiplier at placement)
L-04

fee_bps is stored and validated but never applied in settlement

LOW ACKNOWLEDGED
Description

GameEntry.fee_bps is set by register_game (capped at 1000 bps) and writable via update_config, but is never read by settle_bet or any other instruction. The field is effectively dead.

Impact

Confusing for auditors and integrators reading the code — implies a protocol fee that doesn't exist. No security impact.

Recommendation

Either wire it in (deduct payout × fee_bps / 10000 from payouts and accumulate in a protocol_fee_accrued field, plus a withdrawal instruction) or remove the field in the next account-layout upgrade.

References
state/game_registry.rs:17-18, register_game.rs (validates cap), settle_bet.rs (does not read it)
L-05

InvalidServerSeed error reused for has_one = player mismatches

LOW ACKNOWLEDGED
Description

In settle_bet.rs:77 and reclaim_expired.rs:60, 69, the has_one = player constraint carries the error InvalidServerSeed. A player-mismatch at settlement will surface to clients as "invalid server seed", which is misleading.

Impact

Debug and monitoring ergonomics. No security impact.

Recommendation

Add an InvalidPlayer error code and use it for player-mismatch constraints. InvalidServerSeed should remain exclusive to the SHA-256 check in the handler.

References
settle_bet.rs:77, reclaim_expired.rs:60, reclaim_expired.rs:69
L-06

Settler can self-DoS by spamming submit_commitment for arbitrary wallets

LOW BY DESIGN
Description

submit_commitment uses init_if_needed to create the player's PlayerState PDA, with payer = settler. Nothing stops the settler from calling submit_commitment for randomly generated pubkeys, allocating a PlayerState (~326 bytes, ~2.5 million lamports rent) for each. This is self-harm — the settler is spending its own SOL — but can drain a misconfigured fee-payer budget quickly.

Impact

If the settler's SOL budget is exhausted, legitimate submit_commitment and place_bet_* calls (which require the settler as rent_payer) will fail, effectively pausing the game. The settler's private key is the only thing that can trigger this so it's a self-inflicted wound, not an external attack surface.

Recommendation

Monitor the settler balance and alert below a threshold. Consider moving PlayerState rent to the player (paid at set_referrer or first place_bet) — this pushes rent cost onto the real user who benefits from the state rather than onto the settler.

References
submit_commitment.rs:26-32
07

Informational findings

I-01

has_active_order field in PlayerState is dead code

INFORMATIONAL ACKNOWLEDGED
Description

PlayerState.has_active_order: bool is declared and initialised to false in submit_commitment.rs:50 and set_referrer.rs:58, but never read or toggled. Appears to be vestigial from a design where concurrent bets per player were forbidden. Remove or repurpose.

References
state/player_state.rs:17
I-02

MINIMUM_LIQUIDITY is a flat 10,000 regardless of token decimals

INFORMATIONAL BY DESIGN
Description

Both SOL (9 decimals) and BUX (9 decimals) use MINIMUM_LIQUIDITY = 10_000, which is 0.00001 of either token. Negligible at real deposit sizes. If a future vault is added with 6 decimals, 10,000 base units would represent 0.01 units — still small but proportionally ~1000× larger. Consider scaling MINIMUM_LIQUIDITY by 10^(9 - decimals) when registering new vaults.

References
math.rs:6
I-03

No slippage parameter on deposit/withdraw instructions

INFORMATIONAL ACKNOWLEDGED
Description

Depositors cannot specify min_lp_out; withdrawers cannot specify min_underlying_out. The program uses the LP price computed from live vault state, so a bet settling in the same transaction or block could shift the effective balance and mint the LP at a different rate than the UI quoted. In practice drift is tiny, but MEV-style sandwiching isn't theoretically prevented.

References
deposit_sol.rs, withdraw_sol.rs, deposit_bux.rs, withdraw_bux.rs
I-04

submit_commitment allows arbitrarily large future nonces

INFORMATIONAL BY DESIGN
Description

The require!(nonce >= player_state.nonce, ...) check allows the settler to set pending_nonce to any value greater than or equal to the current. This is intentional (allows pre-commitment batching) but a buggy client could leave the player stuck at a huge nonce with a stale commitment. Consider capping the gap at a small number (say 10) unless an explicit batch flag is passed.

References
submit_commitment.rs:56
I-05

Referral rewards are paid from vault (LP cost), not from a separate fee bucket

INFORMATIONAL BY DESIGN
Description

On loss, referral rewards of up to 1.5% combined (tier-1 + tier-2) are deducted from the same vault that backs LP positions — reducing LPs' share of the house edge. There is no protocol-side accrual or separate treasury. This is a design choice, not a bug, but LPs should understand they are funding the referral programme.

References
settle_bet.rs:192-200, 405-593
I-06

Pause does not block settlement — intentional but should be explicit to LPs

INFORMATIONAL BY DESIGN
Description

The paused flag gates deposits, withdrawals, bet placement, commitment submission, and referrer-setting, but not settle_bet or reclaim_expired. This is correct: pausing must not trap pending bets. But it means that in an incident response scenario where the settler key is believed compromised, pause alone does not stop the settler from continuing to settle bets at attacker-chosen outcomes. Consider adding a separate deep_pause or settle_pause flag for that scenario.

References
settle_bet.rs, reclaim_expired.rs (no paused check)
I-07

UnauthorizedSettler error is raised for authority-role mismatches in pause/update_config

INFORMATIONAL ACKNOWLEDGED
Description

pause.rs:10 and update_config.rs:11 both raise UnauthorizedSettler when the signer doesn't match game_registry.authority. The error message says "Unauthorized settler" but the role being checked is actually authority. Add a distinct UnauthorizedAuthority error code.

References
pause.rs:10, update_config.rs:11
I-08

Events lack bet_order pubkey, requiring indexers to derive it

INFORMATIONAL ACKNOWLEDGED
Description

BetPlaced, BetSettled, and BetReclaimed emit player and nonce, which is sufficient to derive the BetOrder PDA, but not the PDA address directly. Including it would spare indexers the derivation and make logs more self-contained.

References
events.rs:87-114
08

Verified properties

The following properties were verified to hold across all relevant code paths. They are listed here to scope what was tested and to document protections that are in place.

Commit-reveal integrity
Commitment binds seed
settle_bet verifies SHA256(server_seed) == bet_order.commitment_hash, using anchor_lang::solana_program::hash. No path bypasses this check.
Commitment binds bet
place_bet_* copies pending_commitment into bet_order.commitment_hash at placement, clears player_state.pending_commitment, and increments player_state.nonce. Rebinding requires a fresh submit_commitment.
Reveal-only-once
BetOrder closes on settle. The PDA cannot be re-initialised at the same nonce because the nonce is seed material.
Arithmetic safety
Overflow/underflow
All multi-value arithmetic uses checked_add, checked_sub, checked_mul, checked_div on u128 intermediates where necessary. Errors with MathOverflow.
Division by zero
calculate_underlying_for_lp and calculate_lp_price require lp_supply > 0 before dividing. calculate_max_bet_for_difficulty short-circuits to 0 when multiplier_bps == 0 (prevents ÷0).
Precision
LP math truncates consistently. Off-chain Elixir code uses trunc/1 to match.
Signed fields
house_profit and net_pnl_* use i64; checked_add and checked_sub guard overflow in both directions.
Account validation
PDA seeds are canonical
All PDAs use the seeds documented in state/ doc comments. Bumps are stored in GameRegistry for vault/mint PDAs and in account bodies for player/bet.
has_one relationships
BetOrder.has_one = player and has_one = rent_payer correctly bind settlement and reclaim accounts. Hostile accounts cannot be substituted.
Vault authority
SOL vault signs via [b"sol_vault", bump] in CPIs. BUX token account signs via [b"bux_token_account", bump]. LP mint authorities use dedicated seeds. All CPIs are invoke_signed with the correct seeds.
Settler gating
submit_commitment, settle_bet gated on settler.key() == game_registry.settler. update_config, pause, register_game, create_lp_metadata gated on authority.key() == game_registry.authority. Player-facing instructions gated on player signature where appropriate.
Liquidity safety
Deposit donation defence
First deposit burns MINIMUM_LIQUIDITY to prevent first-depositor-frontrun attacks on LP price.
Withdraw cannot exceed available
calculate_underlying_for_lp return is checked against available_balance = vault − rent − total_liability.
Rent-exempt preservation
SOL vault withdrawal math subtracts rent-exempt minimum before computing available_balance, so the vault never loses rent exemption.
Empty pool handling
calculate_lp_price returns LP_PRICE_PRECISION (1.0) when lp_supply == 0, avoiding division by zero on first deposit.
Bet safety
Pre-placement validation
amount >= min_bet, amount <= per-difficulty max, potential_profit <= net_balance. All enforced at placement with reverts.
Nonce replay protection
BetOrder PDA seed includes the nonce; PlayerState.nonce increments monotonically; mismatched nonce reverts with NonceMismatch or fails PDA init.
Settle expiry asymmetry
settle_bet requires elapsed <= bet_timeout; reclaim_expired requires elapsed > bet_timeout. Strict inequality avoids edge-case overlap at the timeout boundary.
Referral failure safety
Transfer failures in referral payout emit ReferralRewardFailed; the main settle_bet does not revert. Payouts to the player are never blocked by referral issues.
Governance constraints
Caps at config time
update_config validates new_game_max_bet_bps <= MAX_BET_BPS (500), new_game_fee_bps <= MAX_FEE_BPS (1000), new_referral_bps <= MAX_SPLIT_BPS (500), tier1+tier2 <= MAX_SPLIT_BPS (500), new_bet_timeout >= MIN_BET_TIMEOUT (60).
Multiplier floor
Non-zero multipliers in update_config must be >= 100 (matches on-chain MULTIPLIER_SCALE of 100, i.e., >= 1×).
Game registry bounded
MAX_GAMES = 10; register_game rejects when full (GameRegistryFull).
Self-referral blocked
set_referrer rejects player == referrer (SelfReferral). Tier-2 loops (tier-2 == player) are zeroed out.
09

Out-of-scope items worth testing separately

  • Settler service code (TypeScript). The off-chain settler holds the private key that can settle every bet. Its correctness and security are as important as the on-chain program's — probably more so in the context of M-01. Recommend an independent review of contracts/blockster-settler/src/services/bankroll-service.ts with focus on: HMAC auth robustness, keypair custody, outcome derivation correctness (byte-to-flip algorithm must match the on-chain expectation in v2), rate limiting, and replay protection.
  • Deployment keys. Program upgrade authority and authority keypair custody. For mainnet we recommend 2-of-3 or 3-of-5 Squads multisig for both.
  • Phoenix LiveView UI. Not financially sensitive in the same way but could show wrong balances or allow bets that would revert. Minor impact.
  • Economic model. Whether the ~1% house edge is sufficient to compensate LPs for the tail risk of the 31.68× multiplier, given expected bet-size distribution.
  • Airdrop program. Separate crate, not touched by this audit.
10

Auditor notes

The codebase reads like an Anchor program written by a developer who has shipped Anchor programs before. Variable naming is consistent, PDA seeds are documented inline, account sizing is explicit with comments, and _reserved padding is everywhere it belongs. The commit-reveal implementation is textbook. The LP math is FateSwap-style and well-tested by the #[cfg(test)] module in math.rs.

The two Medium findings are not surprises — they are design trade-offs the team has already reasoned through. M-01 is a known quantity in casino-on-chain designs: moving outcome logic fully on-chain is expensive in compute units and requires storing prediction data per bet. The current approach gambles (pun intended) that the settler remains honest and operationally robust. Post-M-01 remediation, the program would be genuinely trustless. M-02 is a rotation issue that any production deployment would need to solve via off-chain key-custody hygiene regardless of what the program does.

The Low and Informational findings are ergonomic improvements. None of them exposes current user deposits to risk.

For LP participants evaluating the pool: understand that your risk profile is a function of (a) the program's correctness — reviewed here and clean — and (b) the off-chain settler's correctness and honesty — reviewable but not eliminated without M-01's remediation. Size your deposits accordingly.

Report metadata
Report version1.0
Delivered2026-04-21
Review period2026-04-17 → 2026-04-21
Commit reviewed1d4985d
Branchfeat/solana-migration
Files reviewed24
Lines reviewed~1,200 Rust
Severity classificationOWASP + SCSVS