Skip to content

[Contract]- finalize_event: rank, split, and pay out the prize pool #969

@grantfox-oss

Description

@grantfox-oss

Even with a ranked leaderboard and an escrowed prize pool ,
nothing currently moves XLM out of the contract to winners.
src/token.rs::TokenHelper::distribute_winnings exists but is never called.

Goal

A permissionless finalize_event function that, once a campaign has ended and
every match is resolved, ranks participants, splits the prize pool according to
reward_distribution, and pays the top-N addresses.

Requirements

  1. New EventError (or new FinalizeError) variants:

    • EventNotEnded = 16env.ledger().timestamp() < event.end_time
    • MatchesNotComplete = 17 — at least one of the event's matches has
      result_submitted == false
    • AlreadyFinalized = 18event.is_finalized == true
    • EventCancelled (reuse existing variant)
  2. New function, suggested home src/finalize.rs:

    pub fn finalize_event(env: &Env, caller: Address, event_id: u64) -> Result<Vec<(Address, i128)>, EventError>
    • caller.require_auth()anyone may call this (permissionless, like
      the old verify_event_winners); it just triggers payout once conditions
      are met.
    • Checks, in order: not paused → event exists → not cancelled → not already
      finalized → event.has_ended(now) → all matches resolved.
    • let leaderboard = leaderboard::get_event_leaderboard(env, event_id)?;
    • let n = event.reward_distribution.len();
    • Tie handling at the payout boundary: if leaderboard.len() < n, only
      pay out leaderboard.len() ranks. The unallocated percentage (sum of
      reward_distribution[leaderboard.len()..]) is refunded to event.creator.
    • Equal-points ties within the paid ranks: because Issue 4's leaderboard
      already produces a fully deterministic order (points → exact_scores →
      earliest prediction → address), there are no shared ranks — every
      participant gets a distinct rank and thus a distinct (possibly zero)
      payout. Document this explicitly in the function's doc comment so reviewers
      don't expect "split the rank" logic — determinism is handled upstream.
    • For each rank i in 0..n.min(leaderboard.len()):
      amount = event.prize_pool * reward_distribution[i] as i128 / 100
      TokenHelper::distribute_winnings(env, &xlm_token, &leaderboard[i].user, amount)
    • Remainder/dust: integer division can leave a few stroops undistributed.
      Send any leftover (prize_pool - sum(amounts_paid) - refund_to_creator)
      to event.creator along with their refund (single transfer).
    • Zero participants: if leaderboard.is_empty(), refund the entire
      prize_pool to event.creator.
    • Set event.is_finalized = true, persist.
    • Store a payout snapshot under a new DataKey::EventPayouts(u64)
      Vec<(Address, i128)> for historical queries.
    • Emit (Symbol("event"), Symbol("finalized")) with
      (event_id, winners_paid, total_distributed).
    • Return the payout vec.
  3. New view:

    pub fn get_event_payouts(env: Env, event_id: u64) -> Vec<(Address, i128)>;
  4. Expose finalize_event on the contract in src/lib.rs.

Acceptance criteria

  • Cannot finalize before end_time, or while matches are unresolved, or twice
  • Correct top-N split per reward_distribution, verified against real
    TokenClient::balance changes in tests
  • Fewer participants than reward ranks → unused percentage refunded to creator
  • Zero participants → full refund to creator
  • No XLM is left stranded in the contract after finalization (mod a few
    stroops only if you choose not to send dust anywhere — document the choice
    either way)
  • get_event_payouts returns the stored snapshot

Testing checklist (new tests/finalize_tests.rs)

  • test_finalize_event_distributes_top5_split — 5+ participants, [40,30,20,5,5], assert each winner's balance increases by the correct amount
  • test_finalize_event_before_end_time_rejected
  • test_finalize_event_with_unresolved_match_rejected
  • test_finalize_event_twice_rejected
  • test_finalize_event_fewer_participants_than_ranks_refunds_creator — e.g. 2 participants, 5 ranks
  • test_finalize_event_zero_participants_refunds_full_pool
  • test_finalize_event_zero_prize_pool_noopis_finalized becomes true, no transfers occur
  • test_finalize_event_permissionless — called by a random non-admin, non-creator address succeeds

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions