finalize_event: rank, split, and pay out the prize pool#997
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Collaborator
|
@Bhenzdizma clean job thank you |
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Even with a ranked leaderboard and an escrowed prize pool, nothing previously moved
XLM out of the contract to winners —
TokenHelper::distribute_winningsexisted butwas never called.
This PR adds a permissionless
finalize_eventfunction that, once a campaign hasended and every match is resolved, ranks participants, splits the prize pool according
to
reward_distribution, and pays the top-N addresses. Any unallocated percentage andinteger-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.rsfinalize_event(env, caller, event_id) -> Result<Vec<(Address, i128)>, EventError>caller.require_auth()— anyone may call this (permissionless); it just triggerspayout once the conditions are met.
finalized →
event.has_ended(now)→ all matches resolved.leaderboard::get_event_leaderboard.iin0..n.min(leaderboard.len())(wheren = reward_distribution.len()):amount = prize_pool * reward_distribution[i] / 100, transferred toleaderboard[i].userviaTokenHelper::distribute_winnings.reward ranks, only
leaderboard.len()ranks are paid; the unallocated percentageis refunded to the creator.
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.
bundled into a single creator transfer of
prize_pool - total_distributed, so thecontract balance is exactly
0after finalization.event.is_finalized = trueand persists.DataKey::EventPayouts(event_id)for historicalqueries.
(Symbol("event"), Symbol("finalized"))with(event_id, winners_paid, total_distributed).get_event_payouts(env, event_id) -> Vec<(Address, i128)>— returns the storedpayout snapshot, or an empty vector if the event has not been finalized.
src/event.rsNew
EventErrorvariants:EventNotEnded = 16—now < event.end_timeMatchesNotComplete = 17— at least one match hasresult_submitted == falseAlreadyFinalized = 18—event.is_finalized == trueEventCancelled = 19— event has been cancelled (noEventCancelledvariantexisted on
EventErrorpreviously, so it was added)src/storage_types.rsDataKey::EventPayouts(u64)→Vec<(Address, i128)>for the payout snapshot.src/lib.rsmod finalize.finalize_eventon the contract, with panic mapping consistent with therest of the contract (
contract_paused,event_not_found,event_cancelled,already_finalized,event_not_ended,matches_not_complete,transfer_failed).get_event_payoutsview.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
end_time, while matches are unresolved, or twicereward_distribution, verified against realTokenClient::balancechangescreator refund)
get_event_payoutsreturns the stored snapshotTests (
tests/finalize_tests.rs)All payout amounts are asserted against real
TokenClient::balancedeltas.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_rejectedtest_finalize_event_with_unresolved_match_rejectedtest_finalize_event_twice_rejectedtest_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_pooltest_finalize_event_zero_prize_pool_noop—is_finalizedbecomes true, no transferstest_finalize_event_permissionless— called by a random non-admin, non-creatoraddress succeeds
Test results
cargo buildis clean (exit 0). Full suite passes with no regressions:closes #969