1use rand::random;
2use rand::seq::SliceRandom;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct Contribution {
7 pub recipient: String,
8 pub amount: f64,
9 pub sender: String,
10}
11
12trait Random {
13 fn rnd() -> Self;
14}
15
16impl Random for Contribution {
17 fn rnd() -> Self {
18 let names = ["Alice", "Bob", "Thomas", "Ben", "Jason", "Mary"];
19 let rand_names = names
20 .choose_multiple(&mut rand::thread_rng(), 2)
21 .collect::<Vec<&&str>>();
22 let amount: f64 = random();
23 Contribution {
24 sender: format!("0x{}", rand_names[0]),
25 recipient: format!("0x{}", rand_names[1]),
26 amount,
27 }
28 }
29}
30
31#[derive(Debug)]
32pub struct Matcha {
33 recipient: String,
34 matcha: f64,
35}
36
37#[derive(Default)]
38pub enum MatchingCapStrategy {
39 #[default]
40 Cap,
41 Redistribute,
42}
43
44#[derive(Default)]
45pub struct LinearQfOptions {
46 pub matching_cap_amount: Option<f64>,
47 pub matching_cap_strategy: MatchingCapStrategy,
48 pub upscale: bool,
49}
50
51pub fn calculate_linear_qf(
52 contributions: Vec<Contribution>,
53 matching_pot: f64,
54 options: LinearQfOptions,
55) -> Vec<Matcha> {
56 let mut total_match = 0f64;
57 let mut has_saturated = false;
58 let mut contributions_by_recipient: HashMap<String, HashMap<String, Contribution>> =
59 HashMap::new();
60
61 let mut distributions: Vec<Matcha> = vec![];
62
63 for contribution in contributions {
65 if !contributions_by_recipient.contains_key(&contribution.recipient) {
66 contributions_by_recipient.insert(contribution.recipient.clone(), HashMap::new());
67 }
68
69 if let Some(existing_contribution_map) =
70 contributions_by_recipient.get_mut(&contribution.recipient)
71 {
72 if let Some(existing_contribution) =
73 existing_contribution_map.get_mut(&contribution.sender)
74 {
75 existing_contribution.amount += contribution.amount;
76 } else {
77 existing_contribution_map.insert(contribution.sender.clone(), contribution);
78 }
79 }
80 }
81
82 for details in contributions_by_recipient {
83 let mut sum_of_sqrt_contrib = 0f64;
84 let mut sum_of_contrib = 0f64;
85 for contribution in details.1.values() {
86 sum_of_sqrt_contrib += contribution.amount.powf(0.5);
87 sum_of_contrib += contribution.amount;
88 }
89
90 let matcha = sum_of_sqrt_contrib.powf(2f64) - sum_of_contrib;
91
92 distributions.push(Matcha {
93 recipient: details.0,
94 matcha,
95 });
96
97 total_match += matcha;
98 }
99
100 if total_match > matching_pot {
101 has_saturated = true;
102 }
103
104 if has_saturated {
105 for distribution in &mut distributions {
106 distribution.matcha = (distribution.matcha * matching_pot) / total_match;
107 }
108 }
109
110 if options.upscale && total_match < matching_pot {
111 let upscale_factor = matching_pot / total_match;
112 for distribution in &mut distributions {
113 distribution.matcha *= upscale_factor;
114 }
115 }
116
117 if let Some(cap) = options.matching_cap_amount {
119 let mut overflow_total = 0f64;
120 let mut eligible_for_redistribution = 0usize;
121
122 for matcha in &mut distributions {
124 if matcha.matcha > cap {
125 overflow_total += matcha.matcha - cap;
126 matcha.matcha = cap;
127 } else {
128 eligible_for_redistribution += 1;
129 }
130 }
131
132 if matches!(
134 options.matching_cap_strategy,
135 MatchingCapStrategy::Redistribute
136 ) && overflow_total > 0f64
137 {
138 let redistribution_amount = overflow_total / eligible_for_redistribution as f64;
139 for matcha in &mut distributions {
140 if matcha.matcha < cap {
141 matcha.matcha += redistribution_amount;
142 if matcha.matcha > cap {
144 matcha.matcha = cap;
145 }
146 }
147 }
148 }
149 }
150
151 distributions
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use arr_macro::arr;
158 use rand::*;
159
160 fn generate_contributions() -> Vec<Contribution> {
161 arr![Contribution::rnd(); 10].to_vec()
162 }
163
164 #[test]
165 fn test_upscale_to_pot() {
166 let contributions = arr![Contribution::rnd(); 50].to_vec();
167 let matching_pot = 1000.0;
168 let options = LinearQfOptions {
169 upscale: true,
170 ..Default::default()
171 };
172
173 let distributions = calculate_linear_qf(contributions, matching_pot, options);
174
175 let total_distributed: f64 = distributions.iter().map(|d| d.matcha).sum();
176 assert!(total_distributed.floor() <= matching_pot + f64::EPSILON * 10.0);
177 }
178
179 #[test]
180 fn test_add() {
181 let mut rng = rand::thread_rng();
182
183 let a_contribs = arr![Contribution {
184 recipient: "A".into(),
185 amount: 200f64,
186 sender: rng.gen::<char>().into(),
187 }; 5]
188 .to_vec();
189 let b_contribs = arr![Contribution {
190 recipient: "B".into(),
191 amount: 500f64,
192 sender: rng.gen::<char>().into(),
193 }; 2]
194 .to_vec();
195 let c_contribs = arr![Contribution {
196 recipient: "C".into(),
197 amount: 50f64,
198 sender: rng.gen::<char>().into(),
199 }; 20]
200 .to_vec();
201 let contributions = vec![a_contribs, b_contribs, c_contribs]
202 .into_iter()
203 .flatten()
204 .collect::<Vec<Contribution>>();
205 calculate_linear_qf(contributions, 10_000f64, LinearQfOptions::default());
206 }
207
208 #[test]
209 fn test_redistribution_strategy() {
210 let mut rng = rand::thread_rng();
211
212 let a_contribs = arr![Contribution {
213 recipient: "A".into(),
214 amount: 200f64,
215 sender: rng.gen::<char>().into(),
216 }; 5]
217 .to_vec();
218 let b_contribs = arr![Contribution {
219 recipient: "B".into(),
220 amount: 500f64,
221 sender: rng.gen::<char>().into(),
222 }; 2]
223 .to_vec();
224 let c_contribs = arr![Contribution {
225 recipient: "C".into(),
226 amount: 50f64,
227 sender: rng.gen::<char>().into(),
228 }; 20]
229 .to_vec();
230 let contributions = vec![a_contribs, b_contribs, c_contribs]
231 .into_iter()
232 .flatten()
233 .collect::<Vec<Contribution>>();
234
235 let matching_pot = 10000.0;
236 let cap = 10.0; let options = LinearQfOptions {
239 matching_cap_amount: Some(cap),
240 matching_cap_strategy: MatchingCapStrategy::Redistribute,
241 upscale: false, };
243
244 let distributions = calculate_linear_qf(contributions, matching_pot, options);
245
246 assert!(
248 distributions.iter().all(|d| d.matcha <= cap),
249 "All distributions must be within the cap."
250 );
251
252 let total_distributed: f64 = distributions.iter().map(|d| d.matcha).sum();
254
255 assert!(
257 total_distributed <= matching_pot,
258 "Total distributed amount must not exceed the matching pot."
259 );
260 }
261}