Skip to content

Commit

Permalink
add double lottery to ca rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
zeegomo committed Nov 26, 2021
1 parent 31b4e8e commit 7b65dad
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 57 deletions.
6 changes: 4 additions & 2 deletions src/community_advisors/models/de.rs
Expand Up @@ -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]
Expand All @@ -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(),
Expand Down
21 changes: 12 additions & 9 deletions 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<CommunityAdvisor, TotalTickets>;
pub type CasWinnings = HashMap<CommunityAdvisor, TotalTickets>;
pub type CasWinnings = BTreeMap<CommunityAdvisor, TotalTickets>;

pub fn lottery_distribution<R: Rng>(
distribution: TicketsDistribution,
mut distribution: TicketsDistribution,
tickets_to_distribute: TotalTickets,
rng: &mut R,
) -> CasWinnings {
) -> (CasWinnings, TicketsDistribution) {
let total_tickets = distribution.values().sum::<u64>() as usize;

// Virtually create all tickets and choose the winning tickets using their index.
Expand All @@ -26,13 +26,16 @@ pub fn lottery_distribution<R: Rng>(

// 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)
}
156 changes: 110 additions & 46 deletions src/rewards/community_advisors/mod.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -35,9 +35,11 @@ enum ProposalTickets {
eligible_assessors: BTreeSet<CommunityAdvisor>,
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,
},
}

Expand All @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<R: Rng>(
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::<u64>(),
to_distribute2 + to_distribute1
);
stage1_winners
}

fn calculate_ca_rewards_for_proposal<R: Rng>(
Expand All @@ -174,24 +201,33 @@ fn calculate_ca_rewards_for_proposal<R: Rng>(
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()
Expand Down Expand Up @@ -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"),
}
};
Expand Down Expand Up @@ -343,4 +383,28 @@ mod tests {
);
assert!(are_close(res.values().sum::<Funds>(), 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>(), Funds::from(240)));
assert!(are_close(
*res.get(&excellent_assessor).unwrap(),
Funds::from(120)
));
}
}

0 comments on commit 7b65dad

Please sign in to comment.