Skip to content

Instantly share code, notes, and snippets.

@cNoveron
Last active July 30, 2025 17:20
Show Gist options
  • Save cNoveron/ddd003c8a74aedf4f7e32e9d7a3e870f to your computer and use it in GitHub Desktop.
Save cNoveron/ddd003c8a74aedf4f7e32e9d7a3e870f to your computer and use it in GitHub Desktop.

🧮 Vault Batch Processing Computational Benchmark

KEY FINDINGS

The maximum users per batch in real execution conditions is 59 users as found by the compute units stress limits test. Previously Rowship developer @WalquerX who is in charge of unit test mentioned that the limit lied between 7 and 100 users. Through boundary exploration we found out the hard limit of this implementation of the Vault program is 59 users.

LIMITS ANALYSIS

Compute Unit Limits

After several tests under different conditions computing units per user lie between 23728 and 51852 CU/User. Considering an optimistic computing cost of 23728 CU/User, an average of 37790 CU/User and a pessimistic 51852 CU/User we determine the following limits. Keep in mind that Solana's maximum transaction computing units limit caps transactions at 1.4 million CU.

Scenario Max Batch Total CU
Optimistic 59 1,399,952
Average 37 1,398,230
Pessimistic 26 1,348,152

Account Limits

  • Maximum Limit: 59 users per transaction
  • Primary Bottleneck: Account Limits

Performance Analysis

Here we analyze the different scaling scenarios for Birdshot with different user base sizes alongside feasible batch sizes and the expected execution time for each execution of update_user_balances_batch.

Users Tokens Batches Time Est Feasible
10 3 1 2s ✅ YES
20 8 4 8s ✅ YES
1,000 15 18 36s ✅ YES
5,000 25 59 180s ❌ NO

Key Findings

  • Solana account limit: 59 users
  • Practical safe limit: 27 users
  • Primary bottleneck: Account Limits
  • Memory usage at limit: 21 KB
  • Memory per user: 0.39 KB

Production Recommendations

Considering:

  • User array iteration is causes the computing cost of the transaction to rise proportionally as the number of users per batch grows.
  • Error handling does not account for partial batch failures, transaction executions might fail.

The use of the current vault DOES NOT meet the batch size requirements necessary to handle more than 60 users. It could theoreticallyl be used in production under the follosing conditions:

  • Batch Max Limit: 50 users (although there's still risk of failure)
  • Batch Optimal Size: 27 users
  • Fallback: Reduce batch size if failures occur

As a solution we suggest adding a constant-complexity function to handle the amounts each user can later withdraw addresses the need to iterate over an array of users. Dynamic array iteration makes computing costs unpredictable and causes them to grow proportionally as the batch size increments.

We propose an implementation of a function that only deals with round snapshots as opposed to individually updating every user balance. This is a pattern called 'lazy updating' and it's used by major DeFi protocols like Synthetix, Aave and Compound.

    // === O(1) ALTERNATIVE: Just store round metadata ===
    pub fn finalize_round_o1(
        ctx: Context<UpdateUserBalancesBatch>,
        token_mint: Pubkey,
        total_tokens_received: u64,
    ) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        require_executor_role(&ctx.accounts.caller.key(), vault)?;
        require_stage(vault, VaultStage::Update)?;

        let total_sol_spent = vault.current_round_spend;
        require!(total_sol_spent > 0, VaultErrorCode::InsufficientFunds);

        // O(1): Just store the round snapshot - no user iteration
        vault.round_snapshots.push(RoundSnapshot {
            round: vault.current_round,
            total_sol_spent,
            total_tokens_received,
            token_mint,
        });

        // Update vault's token holdings
        vault.add_or_update_token_balance(token_mint, total_tokens_received)?;

        emit!(RoundFinalized {
            round: vault.current_round,
            token_mint,
            total_sol_spent,
            total_tokens_received,
        });

        Ok(())
    }

The above function has been tested for computational feasibility and significantly reduces the cost of updating the state of the vault after every round. However this requires a function to calculate the pending tokens for a user within the withdraw function.

    pub fn calculate_pending_tokens(&self, vault: &Vault, target_round: u64) -> Result<Vec<TokenBalance>> {
        let mut pending_tokens = Vec::new();
        
        // Process all rounds since last update
        for snapshot in vault.round_snapshots.iter() {
            if snapshot.round > self.last_processed_round && snapshot.round <= target_round {
                let contribution = if self.sol_balance >= self.investment_per_token && self.is_active {
                    self.investment_per_token
                } else {
                    0
                };
                
                if contribution > 0 {
                    let user_fee = (contribution * 2) / 100;
                    let user_net_contribution = contribution - user_fee;
                    let tokens = (user_net_contribution as u128 * snapshot.total_tokens_received as u128 / snapshot.total_sol_spent as u128) as u64;
                    
                    // Add to pending tokens
                    let mut found = false;
                    for pending in pending_tokens.iter_mut() {
                        if pending.mint == snapshot.token_mint {
                            pending.amount += tokens;
                            found = true;
                            break;
                        }
                    }
                    if !found {
                        pending_tokens.push(TokenBalance {
                            mint: snapshot.token_mint,
                            amount: tokens,
                        });
                    }
                }
            }
        }
        
        Ok(pending_tokens)
    }

Considerations

  1. This implementation has not been audited for security yet.
  2. This implementation would require deposits of a fixed amount of SOL (e.g. 5, 10, 20, 50, 100) from users in order to avoid having to iterate over a user-balances array.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment