Skip to content

Competition/bounty payouts capped at 50 winners (MAX_WINNERS_PER_SELECT) — push-based select_winners can't scale #61

Description

@0xdevcollins

Problem

Competition/bounty payouts are hard-capped at 50 winners per event. select_winners rejects any call with more than MAX_WINNERS_PER_SELECT recipients:

  • contracts/events/src/event_ops.rs:25const MAX_WINNERS_PER_SELECT: u32 = 50;
  • contracts/events/src/event_ops.rs:643if winners.len() > MAX_WINNERS_PER_SELECT { return Err(Error::InvalidWinnerPosition); }

This surfaces upstream as a product limit: the app can never let an organizer configure a bounty/competition with more than ~50 prize positions, no matter how the budget is split.

Root cause

select_winners pays every winner in a single atomic transaction (push model). Each winner in the loop does several cross-contract calls plus storage writes (contracts/events/src/event_ops.rs:698-716):

  • escrow::release(...) — token transfer (cross-contract call to the token contract)
  • profile.bootstrap(...) — cross-contract call to the profile contract
  • profile.bump_reputation(...) — cross-contract call
  • a Winner storage entry write

So N winners means N × (token transfer + 2 cross-contract invocations + storage writes) inside one transaction. Soroban imposes hard per-transaction limits on CPU instructions, memory, and the number/size of ledger entries a transaction may read/write. Past some N, the transaction exceeds that budget and reverts.

The 50 cap is a conservative bound chosen to stay safely under those limits — it is a technical ceiling of the current design, not a business rule.

Why we can't just raise the number

Removing or greatly increasing the cap on the current push-based select_winners is unsafe: a large winner list would make the payout transaction exceed Soroban's resource budget and revert every time, leaving the escrowed funds permanently stuck (the escrow can only be settled through this path). A bounded loop is what guarantees the payout always fits in one transaction and the escrow can't be bricked.

Impact

  • Organizers cannot run competitions/bounties that reward more than ~50 participants (e.g. broad quest-style or airdrop-style distributions).
  • The product-layer prize-tier cap (currently 15, see boundless / boundless-nestjs MAX_PRIZE_TIERS) is boxed in by this contract limit — it can be raised at most to 50 without a contract change.

Proposed directions

Scaling beyond ~50 requires changing the payout model from push to pull, not raising the constant:

  1. Pull / claim model — winners claim their own prize in their own transaction (one transaction per winner), so there is no single giant transaction. This contract already implements this pattern for grants/crowdfunding via claim_milestone (contracts/events/src/grant.rs:60); a bounty/competition variant would remove the per-call winner ceiling.
  2. Merkle distributor — store a Merkle root of (recipient, amount) on-chain; each winner claims against a proof. Scales to thousands+ recipients with O(1) on-chain state.

Both are contract redesigns. If unbounded/large winner counts are a real product requirement (quests, airdrops), we should pick one of the above. If bounties remain "podium-style," we may instead just document 50 as the intentional ceiling and align MAX_PRIZE_TIERS to it.

Asks

  • Decide whether large/unbounded winner counts are in scope for bounties/competitions.
  • If yes, choose a payout model (pull-claim vs Merkle distributor) and scope the select_winners redesign.
  • If no, treat 50 as the documented ceiling and cap MAX_PRIZE_TIERS accordingly.

References

  • contracts/events/src/event_ops.rs:25 (MAX_WINNERS_PER_SELECT = 50)
  • contracts/events/src/event_ops.rs:643 (cap check)
  • contracts/events/src/event_ops.rs:698-716 (per-winner payout loop)
  • contracts/events/src/grant.rs:60 (claim_milestone pull-model precedent)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions