Summary
createBatches(array, batchSize) increments the loop counter by batchSize. If a caller (directly or via BatchDistributionConfig.maxRecipientsPerBatch) passes 0, the loop is infinite (i += 0 never advances) and the process hangs. If they pass a negative number, the loop body never executes and the function silently returns [] — i.e. zero transactions, no error — even though the caller asked for a real distribution.
prepareBatchEqualDistribution and prepareBatchWeightedDistribution accept maxRecipientsPerBatch straight from user-supplied config with ?? 100 and pass it through without any sanity check.
Where
packages/sdk/src/utils/batchDistribution.ts:343-351
export function createBatches<T>(array: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0; i < array.length; i += batchSize) { // batchSize=0 → infinite loop
batches.push(array.slice(i, i + batchSize));
}
return batches;
}
packages/sdk/src/utils/batchDistribution.ts:199, 282
const maxRecipientsPerBatch = config.maxRecipientsPerBatch ?? 100;
// ... no validation that maxRecipientsPerBatch > 0 ...
const recipientBatches = createBatches(recipients, maxRecipientsPerBatch);
Impact
- Infinite loop on
0: the entire process (Node script, browser tab) hangs. No way to recover without a kill. Easy to hit by accident with Number(formInput) where the form was empty.
- Silent no-op on negative:
prepareBatch* returns { transactions: [], batchCount: 0 }. Callers who don't check batchCount will believe the distribution succeeded with no on-chain effect.
- Non-integer values (e.g.
100.5) produce overlapping/short slices and double-charge recipients in adjacent batches.
Fix
Validate at the public entry points and in createBatches:
function assertPositiveInt(n: number, name: string): void {
if (!Number.isInteger(n) || n <= 0) {
throw new Error(\`\${name} must be a positive integer (got \${n})\`);
}
}
- Call
assertPositiveInt(maxRecipientsPerBatch, 'config.maxRecipientsPerBatch') at the top of prepareBatchEqualDistribution and prepareBatchWeightedDistribution.
- Add the same check in
createBatches for defense-in-depth (since it's exported).
Acceptance criteria
Summary
createBatches(array, batchSize)increments the loop counter bybatchSize. If a caller (directly or viaBatchDistributionConfig.maxRecipientsPerBatch) passes0, the loop is infinite (i += 0never advances) and the process hangs. If they pass a negative number, the loop body never executes and the function silently returns[]— i.e. zero transactions, no error — even though the caller asked for a real distribution.prepareBatchEqualDistributionandprepareBatchWeightedDistributionacceptmaxRecipientsPerBatchstraight from user-supplied config with?? 100and pass it through without any sanity check.Where
packages/sdk/src/utils/batchDistribution.ts:343-351packages/sdk/src/utils/batchDistribution.ts:199, 282Impact
0: the entire process (Node script, browser tab) hangs. No way to recover without a kill. Easy to hit by accident withNumber(formInput)where the form was empty.prepareBatch*returns{ transactions: [], batchCount: 0 }. Callers who don't checkbatchCountwill believe the distribution succeeded with no on-chain effect.100.5) produce overlapping/short slices and double-charge recipients in adjacent batches.Fix
Validate at the public entry points and in
createBatches:assertPositiveInt(maxRecipientsPerBatch, 'config.maxRecipientsPerBatch')at the top ofprepareBatchEqualDistributionandprepareBatchWeightedDistribution.createBatchesfor defense-in-depth (since it'sexported).Acceptance criteria
createBatches([1,2,3], 0)throws synchronously with a clear message.createBatches([1,2,3], -1)throws.createBatches([1,2,3], 1.5)throws.prepareBatchEqualDistribution/prepareBatchWeightedDistributionreject invalidmaxRecipientsPerBatchbefore any RPC calls.