Skip to content

finalize_event: rank, split, and pay out the prize pool#997

Merged
Olowodarey merged 1 commit into
Arena1X:mainfrom
Bhenzdizma:contrrrr
Jun 19, 2026
Merged

finalize_event: rank, split, and pay out the prize pool#997
Olowodarey merged 1 commit into
Arena1X:mainfrom
Bhenzdizma:contrrrr

Conversation

@Bhenzdizma

Copy link
Copy Markdown
Contributor

Even with a ranked leaderboard and an escrowed prize pool, nothing previously moved
XLM out of the contract to winners — TokenHelper::distribute_winnings existed but
was never called.

This PR adds 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. Any unallocated percentage and
integer-division dust is refunded to the creator in a single transfer, so no XLM is
left stranded in the contract
after finalization.

What changed

New module: src/finalize.rs

  • finalize_event(env, caller, event_id) -> Result<Vec<(Address, i128)>, EventError>
    • caller.require_auth() — anyone may call this (permissionless); it just triggers
      payout once the conditions are met.
    • Checks, in order: not paused → event exists → not cancelled → not already
      finalized → event.has_ended(now) → all matches resolved.
    • Ranks participants via leaderboard::get_event_leaderboard.
    • For each rank i in 0..n.min(leaderboard.len()) (where
      n = reward_distribution.len()):
      amount = prize_pool * reward_distribution[i] / 100, transferred to
      leaderboard[i].user via TokenHelper::distribute_winnings.
    • Tie handling at the payout boundary: if there are fewer participants than
      reward ranks, only leaderboard.len() ranks are paid; the unallocated percentage
      is refunded to the creator.
    • Equal-points ties within paid ranks: the leaderboard is already fully
      deterministic (points → exact_scores → earliest prediction → address), so there
      are no shared ranks — every participant gets a distinct (possibly zero)
      payout. There is intentionally no "split the rank" logic; this is documented in
      the function's doc comment.
    • Remainder/dust: the unallocated percentage and integer-division dust are
      bundled into a single creator transfer of prize_pool - total_distributed, so the
      contract balance is exactly 0 after finalization.
    • Zero participants: the entire prize pool is refunded to the creator.
    • Zero prize pool: no transfers occur, but the event is still marked finalized.
    • Sets event.is_finalized = true and persists.
    • Stores a payout snapshot under DataKey::EventPayouts(event_id) for historical
      queries.
    • Emits (Symbol("event"), Symbol("finalized")) with
      (event_id, winners_paid, total_distributed).
    • Returns the payout vector.
  • get_event_payouts(env, event_id) -> Vec<(Address, i128)> — returns the stored
    payout snapshot, or an empty vector if the event has not been finalized.

src/event.rs

New EventError variants:

  • EventNotEnded = 16now < event.end_time
  • MatchesNotComplete = 17 — at least one match has result_submitted == false
  • AlreadyFinalized = 18event.is_finalized == true
  • EventCancelled = 19 — event has been cancelled (no EventCancelled variant
    existed on EventError previously, so it was added)

src/storage_types.rs

  • Added DataKey::EventPayouts(u64)Vec<(Address, i128)> for the payout snapshot.

src/lib.rs

  • Declared mod finalize.
  • Exposed finalize_event on the contract, with panic mapping consistent with the
    rest of the contract (contract_paused, event_not_found, event_cancelled,
    already_finalized, event_not_ended, matches_not_complete, transfer_failed).
  • Exposed the get_event_payouts view.

Design note: dust handling

Integer division can leave a few stroops undistributed. Rather than leaving them in
the contract, the dust is added to the creator's refund and sent in the same transfer.
The result is that the contract holds exactly 0 XLM for the event after
finalization (verified against real token balances in tests).

Acceptance criteria

  • Cannot finalize before end_time, while matches are unresolved, or twice
  • Correct top-N split per reward_distribution, verified against real
    TokenClient::balance changes
  • Fewer participants than reward ranks → unused percentage refunded to creator
  • Zero participants → full refund to creator
  • No XLM left stranded in the contract after finalization (dust folded into the
    creator refund)
  • get_event_payouts returns the stored snapshot

Tests (tests/finalize_tests.rs)

All payout amounts are asserted against real TokenClient::balance deltas.

  • test_finalize_event_distributes_top5_split — 5 participants, [40,30,20,5,5],
    each winner's balance increases by the correct amount; full pool distributed
  • 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 — 2 participants,
    5 ranks; ranks 3–5 (30%) refunded to creator
  • test_finalize_event_zero_participants_refunds_full_pool
  • test_finalize_event_zero_prize_pool_noopis_finalized becomes true, no transfers
  • test_finalize_event_permissionless — called by a random non-admin, non-creator
    address succeeds

Test results

cargo build is clean (exit 0). Full suite passes with no regressions:

finalize_tests:  8 passed; 0 failed
(full workspace: 234 + all standalone test crates passing, 0 failed)

closes #969

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
insight-arena-4rll Ready Ready Preview, Comment Jun 19, 2026 1:48pm

@Olowodarey

Copy link
Copy Markdown
Collaborator

@Bhenzdizma clean job thank you

@Olowodarey Olowodarey merged commit 203c53c into Arena1X:main Jun 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants