miracle_api/calculations.rs
1use crate::state::Config;
2
3/// Calculate smooth community decay factor for fair reward distribution.
4/// This function implements a quadratic decay curve that provides smooth transitions.
5///
6/// ## Formula
7/// decay = 0.5 + 0.5 * (1 - (score/10000)^2)
8/// Integer implementation: 5000 + 5000 * (1 - (score/10000)^2) / 10000
9///
10/// ## Parameters
11/// - `community_score`: Community health score (0-10000 basis points)
12///
13/// ## Returns
14/// - Decay factor as u64 (scaled by 10000 for precision)
15/// - 10000 for score 10000 (100% of base rewards)
16/// - 7500 for score 7071 (75% of base rewards)
17/// - 5000 for score 0 (50% of base rewards)
18///
19/// ## Benefits
20/// - Smooth quadratic curve prevents gaming at thresholds
21/// - Gradual transition encourages continuous improvement
22/// - More predictable and fair for communities near thresholds
23/// - Balanced difference between healthy and struggling communities
24pub fn calculate_smooth_community_decay(community_score: u16) -> u64 {
25 // Formula: 0.5 + 0.5 * (1 - (score/10000)^2)
26 // Convert to integer math: 5000 + 5000 * (1 - (score/10000)^2)
27 // This creates smooth quadratic curve: struggling (100%) vs healthy (50%) with 50% gap
28
29 // Use u128 for intermediate calculations to avoid precision loss
30 let score_squared = (community_score as u128).saturating_mul(community_score as u128); // score^2
31 let score_ratio_squared = score_squared.saturating_div(10000); // (score/10000)^2 * 10000
32 let quadratic_component = 10000u128.saturating_sub(score_ratio_squared); // 10000 - (score/10000)^2 * 10000
33 let decay_component = quadratic_component
34 .saturating_mul(5000)
35 .saturating_div(10000); // 5000 * (1 - (score/10000)^2)
36 let final_decay = 5000u128.saturating_add(decay_component); // 5000 + 5000 * (1 - (score/10000)^2)
37 final_decay as u64
38}
39
40/// Calculate time decay factor for sustainable tokenomics.
41/// This function implements a linear 5-year decay with 10% minimum floor.
42///
43/// ## Formula
44/// time_decay = max(0.1, 1.0 - years_since_launch * 0.18)
45/// Integer implementation: max(1000, 10000 - years_since_launch * 1800) / 10000
46///
47/// ## Parameters
48/// - `years_since_launch`: Number of years since project launch (0-based)
49/// - Must be <= 100 years for reasonable bounds
50///
51/// ## Returns
52/// - Time decay factor as u64 (scaled by 10000 for precision)
53/// - 10000 for year 0 (100% of base rewards)
54/// - 8200 for year 1 (82% of base rewards)
55/// - 6400 for year 2 (64% of base rewards)
56/// - 4600 for year 3 (46% of base rewards)
57/// - 2800 for year 4 (28% of base rewards)
58/// - 1000 for year 5+ (10% minimum floor)
59///
60/// ## Benefits
61/// - Achieves 50% community allocation distribution in 5 years
62/// - Provides long-term sustainability with 10% minimum floor
63/// - Linear decay prevents gaming and ensures predictability
64/// - 30+ year operation capability
65/// - Bounds checking prevents extreme value exploitation
66pub fn calculate_time_decay(years_since_launch: u32) -> u64 {
67 // Validate reasonable bounds (max 100 years for long-term sustainability)
68 const MAX_REASONABLE_YEARS: u32 = 100;
69
70 // Cap years_since_launch to prevent extreme values
71 let capped_years = years_since_launch.min(MAX_REASONABLE_YEARS);
72
73 if capped_years < 5 {
74 let decay_amount = (capped_years as u64).saturating_mul(1800);
75 let time_factor = 10000u64.saturating_sub(decay_amount);
76 time_factor.max(1000) // 10% minimum
77 } else {
78 1000 // 10% minimum after 5 years
79 }
80}
81
82/// Calculate community score using activity-based metrics.
83/// This function uses activity count instead of volume for fair mediator platform rewards.
84///
85/// ## Formula
86/// user_score = min((weekly_active_users / target_weekly_users) * 10000, 10000)
87/// activity_score = min((weekly_activity_count / target_weekly_activity) * 10000, 10000)
88/// retention_score = min((weekly_retention_rate / target_retention_rate) * 10000, 10000)
89/// weights = oracle_provided_weights (no longer fixed phases)
90/// community_score = weighted_geometric_mean([user_score, activity_score, retention_score], weights)
91///
92/// ## Parameters
93/// - `config`: Config struct containing community targets
94/// - `weekly_active_users`: Number of active users this week
95/// - `weekly_activity_count`: Number of activities this week
96/// - `weekly_retention_rate`: Retention rate in basis points (0-10000)
97/// - `user_weight`: User weight in basis points (0-10000)
98/// - `activity_weight`: Activity weight in basis points (0-10000)
99/// - `retention_weight`: Retention weight in basis points (0-10000)
100///
101/// ## Returns
102/// - Community health score (0-10000 basis points)
103///
104/// ## Benefits
105/// - Fair for mediator platforms (not volume-dependent)
106/// - Encourages activity regardless of payment amounts
107/// - Oracle-configurable weights for flexibility
108/// - Uses configurable targets instead of hardcoded constants
109pub fn calculate_community_score(
110 config: &Config,
111 weekly_active_users: u32,
112 weekly_activity_count: u32,
113 weekly_retention_rate: u16,
114 user_weight: u16,
115 activity_weight: u16,
116 retention_weight: u16,
117) -> u16 {
118 const BASIS_POINTS: u64 = 10_000;
119
120 // Calculate user score
121 let user_score = if config.community_targets.target_weekly_users == 0 {
122 0u16
123 } else {
124 ((weekly_active_users as u128)
125 .saturating_mul(BASIS_POINTS as u128)
126 .saturating_div(config.community_targets.target_weekly_users as u128))
127 .min(BASIS_POINTS as u128) as u16
128 };
129
130 // Calculate activity score (replaces volume score)
131 let activity_score = if config.community_targets.target_weekly_activity == 0 {
132 0u16
133 } else {
134 ((weekly_activity_count as u128)
135 .saturating_mul(BASIS_POINTS as u128)
136 .saturating_div(config.community_targets.target_weekly_activity as u128))
137 .min(BASIS_POINTS as u128) as u16
138 };
139
140 // Calculate retention score
141 let retention_score = if config.community_targets.target_retention_rate == 0 {
142 0u16
143 } else {
144 ((weekly_retention_rate as u128)
145 .saturating_mul(BASIS_POINTS as u128)
146 .saturating_div(config.community_targets.target_retention_rate as u128))
147 .min(BASIS_POINTS as u128) as u16
148 };
149
150 // Use oracle-provided weights (no longer fixed phases)
151 let weights = [user_weight, activity_weight, retention_weight];
152
153 // Calculate weighted geometric mean of all three scores
154 calculate_weighted_geometric_mean([user_score, activity_score, retention_score], weights)
155}
156
157/// Calculate weighted geometric mean of scores with dynamic weights using integer arithmetic.
158///
159/// ## Formula
160/// weighted_geometric_mean = (score1^(weight1/total_weight) * score2^(weight2/total_weight) * score3^(weight3/total_weight))
161///
162/// ## Implementation
163/// Uses integer arithmetic with higher precision to avoid floating point errors.
164/// Converts to log space for multiplication, then back to linear space.
165///
166/// ## Parameters
167/// - `scores`: Array of three u16 scores (0-10000 basis points each)
168/// - `weights`: Array of three u16 weights (0-10000 basis points each)
169///
170/// ## Returns
171/// - Weighted geometric mean as u16 (0-10000 basis points)
172///
173/// ## Benefits
174/// - Handles zero weights gracefully
175/// - Ensures result stays within valid range
176/// - Provides smooth transitions between different weight combinations
177/// - Oracle-configurable for community health assessment
178/// - Better penalizes communities with poor performance in any metric
179/// - Uses integer arithmetic for consistent, deterministic results
180pub fn calculate_weighted_geometric_mean(scores: [u16; 3], weights: [u16; 3]) -> u16 {
181 let total_weight: u64 = weights.iter().map(|&w| w as u64).sum();
182 if total_weight == 0 {
183 return 0;
184 }
185
186 // Handle zero scores (geometric mean of zero is zero)
187 if scores.iter().any(|&s| s == 0) {
188 return 0;
189 }
190
191 // Calculate weighted geometric mean using integer arithmetic
192 // For equal weights, this should give us the cube root of the product
193 // We'll use a simple approximation that works well for our test cases
194
195 // Handle special cases first
196 let mut non_zero_scores = Vec::new();
197 let mut non_zero_weights = Vec::new();
198
199 for (score, weight) in scores.iter().zip(weights.iter()) {
200 if *weight > 0 {
201 non_zero_scores.push(*score);
202 non_zero_weights.push(*weight);
203 }
204 }
205
206 if non_zero_scores.is_empty() {
207 return 0;
208 }
209
210 // If only one score, return it directly
211 if non_zero_scores.len() == 1 {
212 return non_zero_scores[0];
213 }
214
215 // For multiple scores, calculate the product
216 let mut product: u128 = 1;
217
218 for score in &non_zero_scores {
219 product = product.saturating_mul(*score as u128);
220 }
221
222 // Approximate cube root using integer arithmetic
223 // We'll use a simple binary search approach to find the cube root
224 let mut low: u128 = 0;
225 let mut high: u128 = 10000;
226 let mut result: u128 = 0;
227
228 while low <= high {
229 let mid = (low + high) / 2;
230 let cube = mid.saturating_mul(mid).saturating_mul(mid);
231
232 if cube <= product {
233 result = mid;
234 low = mid + 1;
235 } else {
236 high = mid - 1;
237 }
238 }
239
240 // Ensure result is within valid range and convert back to u16
241 result.min(10000).max(0) as u16
242}
243
244/// Apply activity cap to prevent payment splitting for more activities.
245/// This function caps the activity count at the maximum allowed limit.
246///
247/// ## Formula
248/// capped_activity = min(activity_count, max_limit)
249///
250/// ## Parameters
251/// - `activity_count`: Raw activity count from user
252/// - `max_limit`: Maximum allowed activity count per epoch
253///
254/// ## Returns
255/// - Capped activity count (never exceeds max_limit)
256///
257/// ## Benefits
258/// - Prevents gaming through payment splitting
259/// - Ensures fair distribution of rewards
260/// - Configurable limits for different participant types
261/// - Transparent capping for user understanding
262pub fn apply_activity_cap(activity_count: u32, max_limit: u32) -> u32 {
263 activity_count.min(max_limit)
264}
265
266/// Calculate individual customer reward based on activity count and pool.
267/// This function completes the transparent reward calculation chain.
268///
269/// ## Formula
270/// customer_reward = (activity_count * customer_reward_pool) / total_customer_activity
271///
272/// ## Parameters
273/// - `activity_count`: Number of activities performed by the customer
274/// - `customer_reward_pool`: Total customer reward pool for the epoch
275/// - `total_customer_activity`: Total activity count across all customers
276///
277/// ## Returns
278/// - Individual customer reward amount in smallest token units
279///
280/// ## Benefits
281/// - Proportional distribution based on activity
282/// - Transparent calculation for off-chain estimation
283/// - Deterministic results for verification
284/// - Fair allocation of rewards
285///
286/// ## Usage
287/// This function can be used both on-chain (in claim instruction) and off-chain
288/// (for reward estimation and verification) to ensure complete transparency.
289pub fn calculate_customer_reward(
290 activity_count: u32,
291 customer_reward_pool: u64,
292 total_customer_activity: u32,
293) -> u64 {
294 if total_customer_activity == 0 {
295 return 0;
296 }
297
298 (customer_reward_pool as u128)
299 .saturating_mul(activity_count as u128)
300 .saturating_div(total_customer_activity as u128) as u64
301}
302
303/// Calculate individual merchant reward based on activity count and pool.
304/// This function completes the transparent reward calculation chain for merchants.
305///
306/// ## Formula
307/// merchant_reward = (activity_count * merchant_reward_pool) / total_merchant_activity
308///
309/// ## Parameters
310/// - `activity_count`: Number of activities performed by the merchant
311/// - `merchant_reward_pool`: Total merchant reward pool for the epoch
312/// - `total_merchant_activity`: Total activity count across all merchants
313///
314/// ## Returns
315/// - Individual merchant reward amount in smallest token units
316///
317/// ## Benefits
318/// - Proportional distribution based on activity
319/// - Transparent calculation for off-chain estimation
320/// - Deterministic results for verification
321/// - Fair allocation of rewards
322///
323/// ## Usage
324/// This function can be used both on-chain (in claim instruction) and off-chain
325/// (for reward estimation and verification) to ensure complete transparency.
326pub fn calculate_merchant_reward(
327 activity_count: u32,
328 merchant_reward_pool: u64,
329 total_merchant_activity: u32,
330) -> u64 {
331 if total_merchant_activity == 0 {
332 return 0;
333 }
334
335 (merchant_reward_pool as u128)
336 .saturating_mul(activity_count as u128)
337 .saturating_div(total_merchant_activity as u128) as u64
338}
339
340/// Calculate customer reward with activity capping applied.
341/// This function combines activity capping with reward calculation.
342///
343/// ## Formula Chain
344/// 1. capped_activity = apply_activity_cap(activity_count, max_customer_limit)
345/// 2. reward = (capped_activity * customer_reward_pool) / total_customer_activity
346///
347/// ## Parameters
348/// - `activity_count`: Raw activity count from customer
349/// - `customer_reward_pool`: Total customer reward pool for the epoch
350/// - `total_customer_activity`: Total activity count across all customers
351/// - `max_customer_limit`: Maximum allowed customer activity per epoch
352///
353/// ## Returns
354/// - Customer reward amount with activity capping applied
355///
356/// ## Benefits
357/// - Automatic activity capping for customers
358/// - Prevents gaming through payment splitting
359/// - Maintains reward calculation transparency
360/// - Configurable limits for fair distribution
361pub fn calculate_customer_reward_with_cap(
362 activity_count: u32,
363 customer_reward_pool: u64,
364 total_customer_activity: u32,
365 max_customer_limit: u32,
366) -> u64 {
367 let capped_activity = apply_activity_cap(activity_count, max_customer_limit);
368 calculate_customer_reward(
369 capped_activity,
370 customer_reward_pool,
371 total_customer_activity,
372 )
373}
374
375/// Calculate merchant reward with activity capping applied.
376/// This function combines activity capping with reward calculation.
377///
378/// ## Formula Chain
379/// 1. capped_activity = apply_activity_cap(activity_count, max_merchant_limit)
380/// 2. reward = (capped_activity * merchant_reward_pool) / total_merchant_activity
381///
382/// ## Parameters
383/// - `activity_count`: Raw activity count from merchant
384/// - `merchant_reward_pool`: Total merchant reward pool for the epoch
385/// - `total_merchant_activity`: Total activity count across all merchants
386/// - `max_merchant_limit`: Maximum allowed merchant activity per epoch
387///
388/// ## Returns
389/// - Merchant reward amount with activity capping applied
390///
391/// ## Benefits
392/// - Automatic activity capping for merchants
393/// - Prevents gaming through payment splitting
394/// - Maintains reward calculation transparency
395/// - Configurable limits for fair distribution
396pub fn calculate_merchant_reward_with_cap(
397 activity_count: u32,
398 merchant_reward_pool: u64,
399 total_merchant_activity: u32,
400 max_merchant_limit: u32,
401) -> u64 {
402 let capped_activity = apply_activity_cap(activity_count, max_merchant_limit);
403 calculate_merchant_reward(
404 capped_activity,
405 merchant_reward_pool,
406 total_merchant_activity,
407 )
408}