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}