From 7b65dad57b0a416ae4eea90f594c303005f70fe5 Mon Sep 17 00:00:00 2001 From: Giacomo Pasini Date: Fri, 26 Nov 2021 16:50:36 +0100 Subject: [PATCH] add double lottery to ca rewards --- src/community_advisors/models/de.rs | 6 +- src/rewards/community_advisors/lottery.rs | 21 +-- src/rewards/community_advisors/mod.rs | 156 +++++++++++++++------- 3 files changed, 126 insertions(+), 57 deletions(-) diff --git a/src/community_advisors/models/de.rs b/src/community_advisors/models/de.rs index db858315..2b7aaac9 100644 --- a/src/community_advisors/models/de.rs +++ b/src/community_advisors/models/de.rs @@ -57,7 +57,7 @@ mod tests { use super::ReviewScore; use crate::community_advisors::models::AdvisorReviewRow; use crate::utils::csv as csv_utils; - use rand::RngCore; + use rand::{distributions::Alphanumeric, Rng}; use std::path::PathBuf; #[test] @@ -80,7 +80,9 @@ mod tests { AdvisorReviewRow { proposal_id: String::new(), idea_url: String::new(), - assessor: rand::thread_rng().next_u64().to_string(), + assessor: (0..10) + .map(|_| rand::thread_rng().sample(Alphanumeric) as char) + .collect(), impact_alignment_note: String::new(), impact_alignment_rating: 0, feasibility_note: String::new(), diff --git a/src/rewards/community_advisors/lottery.rs b/src/rewards/community_advisors/lottery.rs index df667b51..ce9cad14 100644 --- a/src/rewards/community_advisors/lottery.rs +++ b/src/rewards/community_advisors/lottery.rs @@ -1,16 +1,16 @@ use super::CommunityAdvisor; use rand::Rng; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; pub type TotalTickets = u64; pub type TicketsDistribution = BTreeMap; -pub type CasWinnings = HashMap; +pub type CasWinnings = BTreeMap; pub fn lottery_distribution( - distribution: TicketsDistribution, + mut distribution: TicketsDistribution, tickets_to_distribute: TotalTickets, rng: &mut R, -) -> CasWinnings { +) -> (CasWinnings, TicketsDistribution) { let total_tickets = distribution.values().sum::() as usize; // Virtually create all tickets and choose the winning tickets using their index. @@ -26,13 +26,16 @@ pub fn lottery_distribution( // Consistent iteration is needed to get reproducible results. In this case, // it's ensured by the use of BTreeMap::iter() - for (ca, n_tickets) in distribution.into_iter() { + for (ca, n_tickets) in distribution.iter_mut() { let tickets_won = std::iter::from_fn(|| { - indexes.next_if(|tkt| *tkt < (cumulative_ticket_index + n_tickets) as usize) + indexes.next_if(|tkt| *tkt < (cumulative_ticket_index + *n_tickets) as usize) }) .count(); - cumulative_ticket_index += n_tickets; - winnings.insert(ca, tickets_won as u64); + cumulative_ticket_index += *n_tickets; + if tickets_won > 0 { + winnings.insert(ca.clone(), tickets_won as u64); + } + *n_tickets -= tickets_won as u64; } - winnings + (winnings, distribution) } diff --git a/src/rewards/community_advisors/mod.rs b/src/rewards/community_advisors/mod.rs index b983a850..1790d8ef 100644 --- a/src/rewards/community_advisors/mod.rs +++ b/src/rewards/community_advisors/mod.rs @@ -2,7 +2,7 @@ mod funding; mod lottery; use crate::community_advisors::models::{AdvisorReviewRow, ReviewScore}; -use lottery::TicketsDistribution; +use lottery::{CasWinnings, TicketsDistribution}; use rand::{Rng, SeedableRng}; use rand_chacha::{ChaCha8Rng, ChaChaRng}; @@ -35,9 +35,11 @@ enum ProposalTickets { eligible_assessors: BTreeSet, winning_tkts: u64, }, - Fund6 { - ticket_distribution: TicketsDistribution, - winning_tkts: u64, + Fund7 { + excellent_tkts: TicketsDistribution, + good_tkts: TicketsDistribution, + excellent_winning_tkts: u64, + good_winning_tkts: u64, }, } @@ -64,7 +66,11 @@ fn get_tickets_per_proposal( winning_tkts * (rewards_slots.max_winning_tickets() / LEGACY_MAX_WINNING_TICKETS) } - ProposalTickets::Fund6 { winning_tkts, .. } => winning_tkts, + ProposalTickets::Fund7 { + excellent_winning_tkts, + good_winning_tkts, + .. + } => excellent_winning_tkts + good_winning_tkts, }; (winning_tickets, (id, tickets)) @@ -101,8 +107,13 @@ fn calculate_rewards_per_proposal( / Rewards::from(LEGACY_MAX_WINNING_TICKETS) + bonus_reward / Rewards::from(winning_tkts) } - ProposalTickets::Fund6 { winning_tkts, .. } => { - base_ticket_reward + bonus_reward / Rewards::from(winning_tkts) + ProposalTickets::Fund7 { + excellent_winning_tkts, + good_winning_tkts, + .. + } => { + base_ticket_reward + + bonus_reward / Rewards::from(excellent_winning_tkts + good_winning_tkts) } }; ProposalRewards { @@ -131,38 +142,54 @@ fn load_tickets_from_reviews( }; } - let excellent_reviews = proposal_reviews - .iter() - .filter(|rev| matches!(rev.score(), ReviewScore::Excellent)) - .count() as u64; - let good_reviews = proposal_reviews - .iter() - .filter(|rev| matches!(rev.score(), ReviewScore::Good)) - .count() as u64; - - // assumes only one review per assessor in a single proposal - let ticket_distribution = proposal_reviews - .iter() - .map(|rev| { - let tickets = match rev.score() { - ReviewScore::Excellent => rewards_slots.excellent_slots, - ReviewScore::Good => rewards_slots.good_slots, - _ => unreachable!("we've already filtered out other review scores"), - }; - - (rev.assessor.clone(), tickets) - }) - .collect(); - - let excellent_winning_tkts = - excellent_reviews.min(rewards_slots.max_excellent_reviews) * rewards_slots.excellent_slots; - let good_winning_tkts = - good_reviews.min(rewards_slots.max_good_reviews) * rewards_slots.good_slots; + // assuming only one review per assessor in a single proposal + let (excellent_tkts, good_tkts): (TicketsDistribution, TicketsDistribution) = + // a full match is used so that we don't forget to consider new review types which may be added in the future + proposal_reviews.iter().map(|rev| match rev.score() { + ReviewScore::Excellent => (rev.assessor.clone(), rewards_slots.excellent_slots), + ReviewScore::Good => (rev.assessor.clone(), rewards_slots.good_slots), + _ => unreachable!("we've already filtered out other review scores"), + }).partition(|(_ca, tkts)| *tkts == rewards_slots.excellent_slots); + + let excellent_winning_tkts = std::cmp::min( + excellent_tkts.len() as u64, + rewards_slots.max_excellent_reviews, + ) * rewards_slots.excellent_slots; + let good_winning_tkts = std::cmp::min(good_tkts.len() as u64, rewards_slots.max_good_reviews) + * rewards_slots.good_slots; + + ProposalTickets::Fund7 { + excellent_winning_tkts, + good_winning_tkts, + excellent_tkts, + good_tkts, + } +} - ProposalTickets::Fund6 { - ticket_distribution, - winning_tkts: excellent_winning_tkts + good_winning_tkts, +// Run a two stage lottery to reward community advisors +// +// In the first round, only excellent reviews will be taken into consideration +// In the second, losing tickets from the first round will compete with good review +fn double_lottery( + stage1: TicketsDistribution, + mut stage2: TicketsDistribution, + to_distribute1: u64, + to_distribute2: u64, + rng: &mut R, +) -> CasWinnings { + let (mut stage1_winners, stage1_losers) = + lottery::lottery_distribution(stage1, to_distribute1, rng); + stage2.extend(stage1_losers); + let (stage2_winners, _stage2_losers) = + lottery::lottery_distribution(stage2, to_distribute2, rng); + for (ca, winnings) in stage2_winners { + *stage1_winners.entry(ca).or_default() += winnings; } + assert_eq!( + stage1_winners.values().sum::(), + to_distribute2 + to_distribute1 + ); + stage1_winners } fn calculate_ca_rewards_for_proposal( @@ -174,24 +201,33 @@ fn calculate_ca_rewards_for_proposal( per_ticket_reward, } = proposal_reward; - let (tickets_distribution, tickets_to_distribute) = match tickets { - ProposalTickets::Fund6 { - ticket_distribution, - winning_tkts, - } => (ticket_distribution, winning_tkts), + let rewards = match tickets { + ProposalTickets::Fund7 { + excellent_winning_tkts, + good_winning_tkts, + excellent_tkts, + good_tkts, + } => double_lottery( + excellent_tkts, + good_tkts, + excellent_winning_tkts, + good_winning_tkts, + rng, + ), ProposalTickets::Legacy { eligible_assessors, winning_tkts, } => { - println!("{}", per_ticket_reward); - ( + lottery::lottery_distribution( eligible_assessors.into_iter().map(|ca| (ca, 1)).collect(), winning_tkts, + rng, ) + .0 } }; - lottery::lottery_distribution(tickets_distribution, tickets_to_distribute, rng) + rewards .into_iter() .map(|(ca, tickets_won)| (ca, Rewards::from(tickets_won) * per_ticket_reward)) .collect() @@ -252,7 +288,11 @@ mod tests { ($excellent:expr, $good:expr, $expected:expr) => { let p = gen_dummy_reviews($excellent, $good, 0); match load_tickets_from_reviews(&p, &Default::default()) { - ProposalTickets::Fund6 { winning_tkts, .. } => assert_eq!(winning_tkts, $expected), + ProposalTickets::Fund7 { + excellent_winning_tkts, + good_winning_tkts, + .. + } => assert_eq!(excellent_winning_tkts + good_winning_tkts, $expected), _ => panic!("invalid lottery setup"), } }; @@ -343,4 +383,28 @@ mod tests { ); assert!(are_close(res.values().sum::(), Funds::from(100))); } + + #[test] + fn test_double_stage_lottery() { + let mut proposals = BTreeMap::new(); + let reviews = gen_dummy_reviews(1, 500, 0); // winning tickets: 24 + let excellent_assessor = reviews[0].assessor.clone(); + proposals.insert("1".into(), reviews); + let res = calculate_ca_rewards( + proposals, + &vec![("1".into(), Funds::from(2))].into_iter().collect(), + &FundSetting { + proposal_ratio: 80, + bonus_ratio: 20, + total: Funds::from(240), + }, + &Default::default(), + [0; 32], + ); + assert!(are_close(res.values().sum::(), Funds::from(240))); + assert!(are_close( + *res.get(&excellent_assessor).unwrap(), + Funds::from(120) + )); + } }