frame_plugins/
rewards.rs

1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ```````````````````````````````` REWARD PLUGINS ```````````````````````````````
14// ===============================================================================
15
16//! Defines **pluggable reward models** for computing and distributing value across
17//! participants.
18//!
19//! Rewards are abstracted into two main models:
20//!
21//! ## Payout (`payout`)
22//!
23//! - Computes the **total reward value** to be distributed.
24//! - Produces a single payout amount that acts as the **source value**
25//!   for downstream distribution.
26//!
27//! In this model:
28//! - Input is a scalar representing a measurable quantity (e.g., stake, era, score).
29//! - Output is a **total payout value**.
30//!
31//! Useful for scenarios where:
32//! - The system must determine **how much value is available** for distribution.
33//! - Reward generation follows configurable economic or logical rules.
34//!
35//!
36//! ## Payee (`payee`)
37//!
38//! - Distributes the computed payout among a set of participants.
39//! - Consumes the payout value and allocates it across entities.
40//!
41//! In this model:
42//! - Input is `(Payout, [(Id, Share)])`.
43//! - Output is `[(Id, Payout)]` allocations.
44//!
45//! Useful for scenarios where:
46//! - The total reward must be **split among multiple participants**.
47//! - Allocation depends on contribution, weight, or equal participation.
48//!
49//!
50//! ## Purpose
51//!
52//! Separating reward computation into `payout` and `payee` provides flexibility:
53//!
54//! - **Payout** determines *how much total value* is available.
55//! - **Payee** determines *how that value is distributed*.
56//!
57//! This separation enables:
58//! - Independent evolution of reward generation and distribution strategies.
59//! - Composable reward pipelines.
60//! - Extensibility without modifying existing models.
61
62// ===============================================================================
63// ``````````````````````````````` PAYOUT PLUGINS ````````````````````````````````
64// ===============================================================================
65
66pub use payout::*;
67
68/// Defines **pluggable payout models** for computing the total reward value
69/// from an input signal.
70///
71/// Payouts are abstracted as transformation models that convert an input
72/// quantity into a **single distributable value**.
73///
74/// ## Concept
75///
76/// - A payout model determines **how much total value** should be generated.
77/// - The computed payout acts as the **source value** for downstream distribution.
78///
79/// ## In this model:
80///
81/// - Input is a scalar representing a measurable quantity.
82/// - Output is a **single payout value**.
83///
84/// ## Purpose
85///
86/// Payout models provide flexibility in defining reward generation:
87///
88/// - Control how total rewards are computed from inputs.
89/// - Enable configurable reward policies.
90/// - Serve as the first stage in reward distribution pipelines.
91pub mod payout {
92
93    // ===============================================================================
94    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
95    // ===============================================================================
96
97    // --- Core / Std ---
98    use core::ops::{Shr};
99
100    // --- FRAME Suite ---
101    use frame_suite::{
102        fixedpoint::{FixedForInteger, FixedOp, IntegerToFixed, FixedSignedCast},
103        plugin_model,
104    };
105
106    // --- Substrate primitives ---
107    use sp_runtime::{
108        traits::{Zero, One, CheckedDiv, Bounded}, 
109        Saturating, FixedPointNumber, Vec, FixedI128
110    };
111
112    // ===============================================================================
113    // ````````````````````````````````` ZERO-PAYOUT `````````````````````````````````
114    // ===============================================================================
115
116    plugin_model!(
117        /// A payout model that always returns zero.
118        ///
119        /// ## Use Cases
120        ///
121        /// - Disabling rewards or payouts
122        /// - Testing and benchmarking
123        /// - Placeholder model in non-monetary systems
124        name: pub ZeroPayout,
125        input: Asset,
126        bounds : [Asset: Zero],
127        compute: |_input, _context| {
128            Asset::zero()
129        }
130    );
131
132    // ===============================================================================
133    // ``````````````````````````````` CONSTANT-PAYOUT ```````````````````````````````
134    // ===============================================================================
135
136    /// Defines the configuration for the [`ConstantPayout`] model.
137    ///
138    /// This struct provides a **fixed reward value** that will be returned
139    /// for every computation performed by the model.
140    ///
141    /// **Concept**: **Constant Reward Emission**
142    ///
143    /// Unlike dynamic payout models that depend on input values or contextual
144    /// parameters, this configuration enforces a **static reward policy**.
145
146    pub struct ConstantPayoutConfig<T> {
147        /// The constant reward value returned by the model.
148        pub payout: T,
149    }
150
151    plugin_model!(
152        /// The **ConstantPayout** model returns a **fixed reward value**
153        /// regardless of the provided input.
154        ///
155        /// **Concept**: **Static Reward Model**
156        ///
157        /// This model ignores all input signals and instead produces a
158        /// deterministic output defined entirely by its configuration.
159        ///
160        /// ## Characteristics:
161        /// - **Input-agnostic**: The input value has no effect on the output.
162        /// - **Deterministic**: Always returns the same reward for a given configuration.
163        /// - **Context-driven**: Relies solely on [`ConstantPayoutConfig`] for output.
164        /// - **Zero-complexity**: No computation or aggregation involved.
165        ///
166        ///
167        /// ## Applications:
168        /// - Fixed payout systems (e.g., base rewards, participation rewards)
169        /// - Genesis or bootstrap reward distribution
170        /// - Testing pipelines where predictable output is required
171        ///
172        /// ## Use Cases
173        ///
174        /// - Bootstrap phases where all participants receive equal rewards.
175        /// - Fixed incentive systems with no dependency on performance or input.
176        /// - Testing and benchmarking deterministic payout behavior.
177        ///
178        /// ## Example:
179        /// ```ignore
180        /// let config = ConstantPayoutConfig { init_reward: 100 };
181        /// let output = ConstantPayout::compute((), Some(config));
182        /// assert_eq!(output, 100);
183        /// ```
184        name: pub ConstantPayout,
185        input: Asset,
186        context: ConstantPayoutConfig<Asset>,
187        bounds: [Asset: Copy],
188        compute: |_input, context| {
189            context.payout
190        }
191    );
192
193    // ===============================================================================
194    // `````````````````````````````` INFLATION-PAYOUT ```````````````````````````````
195    // ===============================================================================
196
197    /// Defines the configuration for the [`InflationPayout`] model.
198    ///
199    /// This struct specifies the **inflation rate** used to compute rewards
200    /// as a fraction of the input asset.
201    ///
202    /// **Concept**: **Proportional Inflation-Based Reward**
203    ///
204    /// Rewards are derived by applying a fixed fractional rate to the input,
205    /// enabling linear scaling based on the magnitude of the asset.
206    pub struct InflationPayoutConfig<F>
207    where
208        F: FixedPointNumber,
209    {
210        /// A fixed-point fraction representing the reward rate.
211        ///
212        /// Example: `0.01` represents a 1% reward.
213        pub inflation_rate: F, // fraction, e.g. 0.01 for 1%
214    }
215
216    plugin_model!(
217        /// The **InflationPayout** model computes rewards as a **fixed proportion
218        /// of the input asset**, based on a configured inflation rate.
219        ///
220        /// **Concept**: **Linear Inflation Scaling**
221        ///
222        /// The model converts the input asset into a fixed-point representation,
223        /// applies the inflation rate, and converts the result back into the
224        /// original asset type.
225        ///
226        /// ## Formula
227        ///
228        /// ```text
229        /// reward = input * inflation_rate
230        /// ```
231        ///
232        /// ## Characteristics:
233        /// - **Proportional**: Rewards scale linearly with the input value.
234        /// - **Deterministic**: Same input and rate always produce the same output.
235        /// - **Fixed-point safe**: Uses fixed-point arithmetic for precision.
236        /// - **Context-driven**: Controlled via [`InflationPayoutConfig`].
237        ///
238        /// ## Applications:
239        /// - Staking reward systems
240        /// - Inflationary token supply models
241        /// - Proportional incentive distribution
242        ///
243        /// ## Use Cases
244        ///
245        /// - Token inflation mechanisms
246        /// - Staking rewards proportional to stake
247        /// - Emission schedules with fixed percentage growth
248        ///
249        /// ## Example:
250        /// ```ignore
251        /// let config = InflationPayoutConfig {
252        ///     inflation_rate: FixedU128::from_rational(1, 100)
253        /// }; // 1%
254        /// let reward = InflationPayout::compute(1_000u128, Some(config));
255        /// assert_eq!(reward, 10);
256        /// ```
257        name: pub InflationPayout,
258        input: Asset,
259        others: [FixedPoint],
260        context: InflationPayoutConfig<FixedPoint>,
261        bounds: [
262            Asset: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
263            FixedPoint: FixedPointNumber,
264        ],
265        compute: |input, context| {
266            let x = input.to_fixed();
267            let inflation = context.inflation_rate;
268            let reward_fixed = inflation.saturating_mul(x);
269            Asset::from_fixed(&reward_fixed)
270        }
271    );
272
273    // ===============================================================================
274    // ```````````````````````````````` LINEAR-PAYOUT ````````````````````````````````
275    // ===============================================================================
276
277    /// Defines the configuration for the [`LinearPayout`] model.
278    ///
279    /// This struct specifies the parameters of a **linear reward function**.
280    ///
281    /// The payout is computed as a linear transformation of the input:
282    /// a scaled component plus a constant offset.
283    pub struct LinearPayoutConfig<F>
284    where
285        F: FixedPointNumber,
286    {
287        /// The scaling factor applied to the input.
288        pub slope: F,
289        /// The constant offset added to the result.
290        pub base_reward: F,
291    }
292
293    plugin_model!(
294        /// The **LinearPayout** model computes rewards using a
295        /// **linear function** of the input asset.
296        ///
297        /// **Concept**: **Linear Transformation**
298        ///
299        /// ## Formula
300        ///
301        /// ```text
302        /// reward = (slope * input) + base_reward
303        /// ```
304        ///
305        /// ## Characteristics:
306        /// - **Linear scaling**: Reward grows proportionally with input.
307        /// - **Base offset**: Ensures a minimum reward via `base_reward`.
308        /// - **Deterministic**: Same inputs and parameters yield identical results.
309        /// - **Fixed-point safe**: Uses fixed-point arithmetic for precision.
310        /// - **Context-driven**: Controlled via [`LinearPayoutConfig`].
311        ///
312        /// ## Applications:
313        /// - Staking systems with base + proportional rewards
314        /// - Incentive models with guaranteed minimum payout
315        /// - Linear emission schedules
316        /// - Reward shaping for participation-based systems
317        ///
318        /// ## Use Cases
319        ///
320        /// - Reward systems with a base incentive plus proportional scaling
321        /// - Gradual incentive curves
322        /// - Configurable emission policies
323        ///
324        /// ## Example:
325        /// ```ignore
326        /// let config = LinearPayoutConfig {
327        ///     slope: FixedU128::from_rational(1, 10), // 0.1x
328        ///     base_reward: FixedU128::from_integer(5),
329        /// };
330        ///
331        /// let reward = LinearPayout::compute(100u128, Some(config));
332        /// // reward = (0.1 * 100) + 5 = 15
333        /// assert_eq!(reward, 15);
334        /// ```
335        name: pub LinearPayout,
336        input: Asset,
337        others: [FixedPoint],
338        context: LinearPayoutConfig<FixedPoint>,
339        bounds: [
340            Asset: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
341            FixedPoint: FixedPointNumber,
342        ],
343        compute: |input, context| {
344            let slope = context.slope;
345            let base = context.base_reward;
346            let x = input.to_fixed();
347            let reward_fixed = x.saturating_mul(slope).saturating_add(base);
348            Asset::from_fixed(&reward_fixed)
349        }
350    );
351
352    // ===============================================================================
353    // ``````````````````````````````` QUADRATIC-PAYOUT ``````````````````````````````
354    // ===============================================================================
355
356    /// Defines the configuration for the [`QuadraticPayout`] model.
357    ///
358    /// This struct specifies the coefficients of a **quadratic reward function**,
359    /// enabling non-linear reward shaping.
360    ///
361    /// **Concept**: **Quadratic Reward Curve**
362    ///
363    /// Rewards are computed using a second-degree polynomial:
364    ///
365    /// ```text
366    /// reward = (a * x^2) + (b * x) + c
367    /// ```
368    ///
369    /// This allows flexible modeling of reward behavior:
370    ///
371    /// - **Convex curve (a > 0):** Rewards accelerate as input increases  
372    ///   -> Encourages high participation or large stake  
373    ///
374    /// - **Concave curve (a < 0):** Rewards grow sublinearly  
375    ///   -> Penalizes concentration, discourages dominance  
376    ///
377    /// - **Linear case (a = 0):** Reduces to a linear function  
378    pub struct QuadraticPayoutConfig<F>
379    where
380        F: FixedPointNumber,
381    {
382        /// Controls curvature (growth acceleration/decay)
383        pub quadratic_coeff: F,
384        /// Controls proportional scaling
385        pub linear_coeff: F,
386        /// Base reward offset
387        pub constant_term: F,
388    }
389
390    plugin_model!(
391        /// The **QuadraticPayout** model computes rewards using a **quadratic
392        /// function** of the input asset.
393        ///
394        /// **Concept**: **Second-Order Reward Transformation**
395        ///
396        /// ## Formula
397        ///
398        /// ```text
399        /// reward = (a * x^2) + (b * x) + c
400        /// ```
401        ///
402        /// ## Characteristics:
403        /// - **Non-linear scaling**: Captures accelerating or diminishing returns
404        /// - **Flexible shaping**: Controlled via three coefficients
405        /// - **Deterministic**: Same input and parameters yield identical results
406        /// - **Fixed-point safe**: Uses fixed-point arithmetic for precision
407        /// - **Context-driven**: Controlled via [`QuadraticPayoutConfig`]
408        ///
409        /// ## Applications:
410        /// - Advanced staking reward curves
411        /// - Anti-centralization incentive models
412        /// - Economic simulations and experimentation
413        /// - Reward shaping in governance systems
414        ///
415        /// ## Use Cases
416        ///
417        /// - Anti-whale reward shaping (concave curves)
418        /// - Incentivizing large contributions (convex curves)
419        /// - Flexible economic modeling beyond linear systems
420        /// - Approximation of more complex reward curves
421        ///
422        /// ## Example
423        ///
424        /// ```ignore
425        /// let config = QuadraticPayoutConfig {
426        ///     quadratic_coeff: FixedU128::from_rational(1, 100), // 0.01
427        ///     linear_coeff: FixedU128::from_integer(2),          // 2x
428        ///     constant_term: FixedU128::from_integer(10),        // base reward
429        /// };
430        ///
431        /// let reward = QuadraticPayout::compute(100u128, Some(config));
432        /// // reward = (0.01 * 100^2) + (2 * 100) + 10 = 100 + 200 + 10 = 310
433        /// ```
434        name: pub QuadraticPayout,
435        input: Asset,
436        others: [FixedPoint],
437        context: QuadraticPayoutConfig<FixedPoint>,
438        bounds: [
439            Asset: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
440            FixedPoint: FixedPointNumber,
441        ],
442        compute: |input, context| {
443            let a = context.quadratic_coeff;
444            let b = context.linear_coeff;
445            let c = context.constant_term;
446
447            let x = input.to_fixed();
448
449            // Compute a * x^2
450            let x_sq = x.saturating_mul(x);
451            let term_quadratic = a.saturating_mul(x_sq);
452
453            // Compute b * x
454            let term_linear = b.saturating_mul(x);
455
456            // Constant term
457            let term_constant = c;
458
459            // Combine all terms
460            let reward_fixed = term_quadratic
461                .saturating_add(term_linear)
462                .saturating_add(term_constant);
463
464            Asset::from_fixed(&reward_fixed)
465        }
466    );
467
468    // ===============================================================================
469    // ``````````````````````````````` HALVING-PAYOUT ````````````````````````````````
470    // ===============================================================================
471
472    /// Defines the configuration for the [`HalvingPayout`] model.
473    ///
474    /// This struct specifies the parameters for a **halving-based reward schedule**,
475    /// where rewards decrease exponentially over time.
476    ///
477    /// **Concept**: **Exponential Decay via Halving**
478    ///
479    /// Rewards follow a discrete exponential decay pattern:
480    ///
481    /// ```text
482    /// reward = R0 / 2^n
483    /// ```
484    ///
485    /// where:
486    /// - `R0` = initial reward
487    /// - `n`  = number of halving intervals (e.g., era, epoch, or block index)
488    ///
489    /// This model is widely used in monetary systems to:
490    /// - Control long-term inflation
491    /// - Gradually reduce issuance
492    /// - Introduce scarcity over time
493    pub struct HalvingPayoutConfig<T> {
494        /// Initial reward (R0): payout when n = 0
495        pub initial_reward: T,
496    }
497
498    plugin_model!(
499        /// The **HalvingPayout** model computes rewards using a **binary
500        /// exponential decay** based on the input interval.
501        ///
502        /// **Concept**: **Discrete Halving Function**
503        ///
504        /// ## Formula
505        ///
506        /// ```text
507        /// reward = R0 / 2^n
508        /// ```
509        ///
510        /// ## Characteristics:
511        /// - **Exponential decay**: Reward halves with each increment of input
512        /// - **Deterministic**: Same input and configuration yield identical output
513        /// - **Efficient**: Uses bit shifting instead of division
514        /// - **Discrete**: Stepwise reduction per interval
515        /// - **Context-driven**: Controlled via [`HalvingPayoutConfig`]
516        ///
517        /// ## Applications:
518        /// - Blockchain issuance schedules
519        /// - Mining or staking reward decay
520        /// - Long-term economic stabilization
521        /// - Scarcity-driven incentive design
522        ///
523        /// ## Behavior
524        ///
525        /// - At `n = 0`: reward = `R0`
526        /// - At `n = 1`: reward = `R0 / 2`
527        /// - At `n = 2`: reward = `R0 / 4`
528        /// - ...
529        ///
530        /// ## Use Cases
531        ///
532        /// - Bitcoin-style emission schedules
533        /// - Deflationary tokenomics
534        /// - Long-term reward tapering
535        /// - Controlled supply issuance
536        ///
537        /// ## Example
538        ///
539        /// ```ignore
540        /// let config = HalvingPayoutConfig {
541        ///     initial_reward: 100,
542        /// };
543        ///
544        /// assert_eq!(HalvingPayout::compute(0, Some(config)), 100); // 100 / 2^0
545        /// assert_eq!(HalvingPayout::compute(1, Some(config)), 50);  // 100 / 2^1
546        /// assert_eq!(HalvingPayout::compute(2, Some(config)), 25);  // 100 / 2^2
547        /// ```
548        name: pub HalvingPayout,
549        input: Asset,  // input: halving index (n), output: reward
550        context: HalvingPayoutConfig<Asset>,
551        bounds: [
552            Asset: Copy + Shr<Output = Asset> + Zero
553        ],
554        compute: |n, context| {
555            // Special case: n = 0 -> return initial reward
556            if n.is_zero() {
557                return context.initial_reward
558            }
559
560            // Compute: R0 >> n  ==  R0 / 2^n
561            context.initial_reward >> n
562        }
563    );
564
565    // ===============================================================================
566    // ``````````````````````````````` EXP-DECAY-PAYOUT ``````````````````````````````
567    // ===============================================================================
568
569    /// Defines the configuration for the [`ExpDecayPayout`] model.
570    ///
571    /// This struct specifies the parameters for an **exponential decay reward function**,
572    /// where rewards decrease continuously over time.
573    ///
574    /// **Concept**: **Continuous Exponential Decay**
575    ///
576    /// Rewards follow a smooth exponential decay curve:
577    ///
578    /// ```text
579    /// reward = r0 * e^(-a * x)
580    /// ```
581    ///
582    /// where:
583    /// - `r0` = initial reward
584    /// - `a`  = decay constant (rate of decay, a > 0)
585    /// - `x`  = input variable (e.g., time, era, or block index)
586    ///
587    /// This model enables:
588    /// - Smooth reward reduction over time
589    /// - More natural decay compared to discrete halving
590    /// - Fine-grained control over emission rate
591    pub struct ExpDecayPayoutConfig<T, F>
592    where
593        F: FixedPointNumber,
594    {
595        /// Initial reward (r0): reward at x = 0
596        pub initial_reward: T,
597
598        /// Decay constant (a): controls how fast rewards decrease
599        pub decay_constant: F,
600    }
601
602    plugin_model!(
603        /// The **ExpDecayPayout** model computes rewards using a **continuous
604        /// exponential decay** based on the input variable.
605        ///
606        /// **Concept**: **Smooth Decay Function**
607        ///
608        /// ## Formula
609        ///
610        /// ```text
611        /// reward = r0 * e^(-a * x)
612        /// ```
613        ///
614        /// ## Signed Arithmetic
615        ///
616        /// The exponent `-a * x` is always non-positive for `a >= 0` and `x >= 0`.
617        /// Unsigned fixed-point types cannot represent negative numbers, so the
618        /// negation is performed inside a concrete `FixedI128` workspace via
619        /// [`FixedSignedCast`], then projected back. This makes the model correct
620        /// for both signed and unsigned `Asset` and `FixedPoint` types.
621        ///
622        /// ## Characteristics:
623        /// - **Continuous decay**: Smooth reduction instead of stepwise halving.
624        /// - **Non-linear scaling**: Faster decay as `a` increases.
625        /// - **Deterministic**: Same input and parameters yield identical results.
626        /// - **Fixed-point safe**: Signed intermediate arithmetic via `FixedSignedCast`.
627        /// - **Works with unsigned types**: No `Neg` bound; negation in `FixedI128`.
628        /// - **Context-driven**: Controlled via [`ExpDecayPayoutConfig`].
629        ///
630        /// ## Applications:
631        /// - Emission schedules with smooth decay.
632        /// - Staking reward tapering.
633        /// - Time-based incentive reduction.
634        /// - Economic stabilization mechanisms.
635        ///
636        /// ## Use Cases
637        /// - Replacing halving with a smoother continuous decay.
638        /// - Gradual reward reduction without abrupt drops.
639        /// - Fine-tuned monetary policy control.
640        ///
641        /// ## Example
642        ///
643        /// ```ignore
644        /// let config = ExpDecayPayoutConfig {
645        ///     initial_reward: 1000u128,
646        ///     decay_constant: FixedU128::saturating_from_rational(1, 10), // a = 0.1
647        /// };
648        /// // x = 10: reward = 1000 * e^(-1.0) ~= 367
649        /// assert_eq!(ExpDecayPayout::compute(10u128, Some(config)), 367);
650        /// ```
651        name: pub ExpDecayPayout,
652        input: Asset,   // x: time / era / block index
653        others: [FixedPoint],
654        context: ExpDecayPayoutConfig<Asset, FixedPoint>,
655        bounds: [
656            Asset: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
657            FixedPoint: FixedPointNumber + FixedSignedCast<Signed = FixedI128>,
658        ],
659        compute: |x, context| {
660            // Lift input and decay constant into the signed workspace.
661            // saturated_into is infallible for FixedU64 (u64 always fits in i128)
662            // and clamps at i128::MAX for large FixedU128 values. For signed
663            // FixedPoint types it is a zero-cost identity.
664            let x_fixed:  FixedPoint = x.to_fixed();
665            let x_s:      FixedI128  = FixedSignedCast::saturated_into(x_fixed);
666            let a_s:      FixedI128  = FixedSignedCast::saturated_into(context.decay_constant);
667    
668            // a * x in signed space - always >= 0 when a >= 0 and x >= 0.
669            let ax_s: FixedI128 = a_s.saturating_mul(x_s);
670    
671            // Negate to produce the exponent -a * x.
672            // saturating_sub from zero avoids any dependency on Neg being
673            // implemented for FixedPoint, which unsigned types do not satisfy.
674            let neg_ax: FixedI128 = FixedI128::zero().saturating_sub(ax_s);
675    
676            // e^(-a * x), result is in (0, 1] for non-negative a and x.
677            // Concrete FixedI128::fixed_exp - no generic FixedOp bound needed.
678            // unwrap_or(zero) is a safe sentinel, overflow is not reachable here
679            // since the exponent is <= 0.
680            let exp_s: FixedI128 = FixedI128::fixed_exp(&neg_ax)
681                .unwrap_or(FixedI128::zero());
682    
683            // Project the exp result back to FixedPoint.
684            // exp_s is always in (0, 1] so it is non-negative and representable
685            // in both signed and unsigned FixedPoint types.
686            let exp_fp: FixedPoint = FixedSignedCast::saturated_from(exp_s);
687    
688            // reward = r0 * e^(-a * x)
689            let r0_fixed: FixedPoint = context.initial_reward.to_fixed();
690            let reward_fixed: FixedPoint = r0_fixed.saturating_mul(exp_fp);
691    
692            Asset::from_fixed(&reward_fixed)
693        }
694    );
695 
696
697    // ===============================================================================
698    // ``````````````````````````````` SIGMOID-PAYOUT ````````````````````````````````
699    // ===============================================================================
700
701    /// Defines the configuration for the [`SigmoidPayout`] model.
702    ///
703    /// This struct specifies the parameters for a **sigmoid (S-curve) reward function**,
704    /// enabling smooth growth and saturation behavior.
705    ///
706    /// **Concept**: **Logistic Growth Curve**
707    ///
708    /// Rewards follow a sigmoid function:
709    ///
710    /// ```text
711    /// reward = L / (1 + e^(-k(x - x0)))
712    /// ```
713    ///
714    /// where:
715    /// - `L`   = maximum reward (upper bound)
716    /// - `k`   = growth rate (steepness of the curve)
717    /// - `x0`  = midpoint (inflection point)
718    /// - `x`   = input variable (e.g., time, era, or score)
719    ///
720    /// Instead of directly specifying `k` and `x0`, this model derives them from:
721    /// - `growth_start` (a): lower percentile of growth (0 < a < 1)
722    /// - `growth_end`   (b): upper percentile of growth (0 < b < 1)
723    ///
724    /// This allows intuitive configuration of the curve shape.
725    pub struct SigmoidPayoutConfig<T, F>
726    where
727        F: FixedPointNumber,
728    {
729        /// Maximum reward (L): asymptotic upper bound
730        pub max_reward: T,
731
732        /// Lower growth bound (a): fraction where growth begins (0 < a < 1)
733        pub growth_start: F,
734
735        /// Upper growth bound (b): fraction where growth saturates (0 < b < 1)
736        pub growth_end: F,
737    }
738
739    plugin_model!(
740        /// The **SigmoidPayout** model computes rewards using a **logistic
741        /// (S-shaped) function**, producing slow start, rapid growth, and
742        /// eventual saturation.
743        ///
744        /// **Concept**: **S-Curve Reward Transformation**
745        ///
746        /// ## Formula
747        ///
748        /// ```text
749        /// f(x) = L / (1 + e^(-k*(x - x0)))
750        /// ```
751        ///
752        /// where `k` and `x0` are derived from `growth_start` (a) and `growth_end` (b):
753        ///
754        /// ```text
755        /// k  = logit(b) - logit(a)    (always > 0 when b > a, both in (0, 1))
756        /// x0 = -logit(a) / k          (midpoint; > 0 when a < 0.5)
757        /// ```
758        ///
759        /// ## Signed Arithmetic
760        ///
761        /// Even when `Asset` and `FixedPoint` are unsigned types, several
762        /// intermediate values are inherently signed:
763        ///
764        /// - `logit(a) < 0` for any `a < 0.5`.
765        /// - `x - x0 < 0` for any `x < x0` (the entire left half of the curve).
766        /// - `-k*(x - x0) < 0` for any `x > x0` (the entire right half).
767        ///
768        /// All of these are computed in a concrete `FixedI128` workspace via
769        /// [`FixedSignedCast`], then the final result (always >= 0) is projected
770        /// back. This makes the model correct for both signed and unsigned types
771        /// with no `Neg` bound on `FixedPoint`.
772        ///
773        /// ## Guard Conditions (returns zero)
774        ///
775        /// - `growth_start <= 0` or `growth_start >= 1`.
776        /// - `growth_end   <= 0` or `growth_end   >= 1`.
777        /// - `k == 0` (degenerate; only when `growth_start == growth_end`).
778        ///
779        /// ## Precision Note
780        ///
781        /// Fixed-point arithmetic accumulates small rounding errors across the
782        /// `ln -> k -> x0 -> exp` chain. In practice the output may be one integer
783        /// unit below the analytically exact value at certain inputs. This is
784        /// expected and inconsequential for integer rewards.
785        ///
786        /// ## Characteristics:
787        /// - **Bounded**: Reward never exceeds `max_reward`.
788        /// - **Smooth growth**: Gradual ramp-up instead of abrupt changes.
789        /// - **Works with unsigned types**: No `Neg` bound; negation in `FixedI128`.
790        /// - **Deterministic**: Same input and parameters yield identical results.
791        /// - **Context-driven**: Controlled via [`SigmoidPayoutConfig`].
792        ///
793        /// ## Applications:
794        /// - Adoption-based reward curves.
795        /// - Incentive ramp-up systems.
796        /// - Gradual onboarding rewards.
797        /// - Supply emission with saturation.
798        ///
799        /// ## Example
800        ///
801        /// ```ignore
802        /// let config = SigmoidPayoutConfig {
803        ///     max_reward:   100u128,
804        ///     growth_start: FixedU128::saturating_from_rational(1, 10), // 0.1
805        ///     growth_end:   FixedU128::saturating_from_rational(9, 10), // 0.9
806        /// };
807        /// // k ~= 4.394, x0 ~= 0.5
808        /// // f(0) = 10  (= growth_start * L, lower tail)
809        /// // f(1) = 89  (= growth_end * L, upper tail, rounded down)
810        /// assert_eq!(SigmoidPayout::compute(0u128, Some(config)), 10);
811        /// assert_eq!(SigmoidPayout::compute(1u128, Some(config)), 89);
812        /// ```
813        name: pub SigmoidPayout,
814        input: Asset,
815        others: [FixedPoint],
816        context: SigmoidPayoutConfig<Asset, FixedPoint>,
817        bounds: [
818            Asset: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Zero,
819            FixedPoint: FixedPointNumber + FixedSignedCast<Signed = FixedI128>,
820        ],
821        compute: |x, context| {
822            let zero_fp = FixedPoint::zero();
823            let one_fp  = FixedPoint::one();
824            let zero_s  = FixedI128::zero();
825    
826            let a = context.growth_start;
827            let b = context.growth_end;
828    
829            // Guard: a and b must be strictly in (0, 1).
830            if a <= zero_fp || a >= one_fp || b <= zero_fp || b >= one_fp {
831                return Asset::zero();
832            }
833    
834            // logit(p) = ln(p / (1 - p))
835            //
836            // The ratio p/(1-p) is always > 0 and is computed in FixedPoint space.
837            // It is promoted to FixedI128 before calling fixed_ln because the result
838            // can be negative when p < 0.5 (ratio < 1, ln < 0).
839            // Concrete FixedI128::fixed_ln - no generic FixedOp bound needed.
840            let logit = |p: FixedPoint| -> Option<FixedI128> {
841                let denom = one_fp.saturating_sub(p);    // 1 - p > 0 since p < 1
842                let ratio = p.checked_div(&denom)?;       // p/(1-p) > 0
843                let ratio_s: FixedI128 = FixedSignedCast::saturated_into(ratio);
844                FixedI128::fixed_ln(&ratio_s)             // may be negative
845            };
846    
847            let logit_a: FixedI128 = match logit(a) {
848                Some(v) => v,
849                None    => return Asset::zero(),
850            };
851            let logit_b: FixedI128 = match logit(b) {
852                Some(v) => v,
853                None    => return Asset::zero(),
854            };
855    
856            // k = logit(b) - logit(a), always > 0 when b > a.
857            let k: FixedI128 = logit_b.saturating_sub(logit_a);
858            if k == zero_s {
859                return Asset::zero();
860            }
861    
862            // x0 = -logit(a) / k
863            //
864            // The negation is required by the derivation: solving f(0) = a * L gives
865            // k * x0 = -logit(a), so x0 = -logit(a) / k. For a < 0.5, logit(a) < 0,
866            // so -logit(a) > 0 and x0 > 0. The original code omitted the negation,
867            // placing x0 at a negative value and collapsing the curve to its
868            // saturation tail at x = 0.
869            let neg_logit_a: FixedI128 = zero_s.saturating_sub(logit_a);
870            let x0: FixedI128 = match neg_logit_a.checked_div(&k) {
871                Some(v) => v,
872                None    => return Asset::zero(),
873            };
874    
875            // Evaluate f(x) = L / (1 + e^(-k*(x - x0))).
876            // All arithmetic stays in FixedI128 so that x - x0 can be negative
877            // when x < x0, and the negated product can be negative when x > x0.
878            let x_s:     FixedI128 = FixedSignedCast::saturated_into(x.to_fixed());
879            let delta:   FixedI128 = x_s.saturating_sub(x0);
880            let k_delta: FixedI128 = k.saturating_mul(delta);
881    
882            // Negate via subtraction from zero
883            let neg_k_delta: FixedI128 = zero_s.saturating_sub(k_delta);
884    
885            // Concrete FixedI128::fixed_exp - no generic bound needed.
886            // On extreme overflow uses max_value() so the denominator is large and
887            // the result rounds toward zero rather than producing garbage.
888            let exp_val: FixedI128 = FixedI128::fixed_exp(&neg_k_delta)
889                .unwrap_or(FixedI128::max_value());
890    
891            let denom: FixedI128 = FixedI128::one().saturating_add(exp_val);
892    
893            let l_s: FixedI128 = FixedSignedCast::saturated_into(context.max_reward.to_fixed());
894            let output_s: FixedI128 = l_s.checked_div(&denom).unwrap_or(zero_s);
895    
896            // Project back to FixedPoint.
897            // output_s >= 0 because L >= 0 and denom >= 1; saturated_from clamps
898            // any accidental negative to zero rather than panicking.
899            let output_fp: FixedPoint = FixedSignedCast::saturated_from(output_s);
900            Asset::from_fixed(&output_fp)
901        }
902    );
903
904    // ===============================================================================
905    // ````````````````````````` INVERSE-PROPORTIONAL-PAYOUT `````````````````````````
906    // ===============================================================================
907
908    /// Defines the configuration for the [`InverseProportionalPayout`] model.
909    ///
910    /// This struct specifies the parameters for an **inverse proportional reward
911    /// function**, where rewards decrease as the input increases.
912    ///
913    /// **Concept**: **Inverse Scaling Function**
914    ///
915    /// Rewards follow an inverse relationship:
916    ///
917    /// ```text
918    /// reward = k / (x + eps)
919    /// ```
920    ///
921    /// where:
922    /// - `k` = proportionality constant (scaling factor)
923    /// - `x` = input variable (e.g., stake, time, or score)
924    /// - `eps` = small positive constant to prevent division by zero
925    ///
926    /// This model produces high rewards for small inputs and diminishing rewards
927    /// as input increases.
928    pub struct InverseProportionalConfig<F>
929    where
930        F: FixedPointNumber,
931    {
932        /// Proportionality constant (k): controls overall reward scale
933        pub k: F,
934
935        /// Small constant (eps): prevents division by zero and stabilizes
936        /// behavior near x = 0
937        pub epsilon: F,
938    }
939
940    plugin_model!(
941        /// The **InverseProportionalPayout** model computes rewards using an
942        /// **inverse function** of the input asset.
943        ///
944        /// **Concept**: **Diminishing Reward Curve**
945        ///
946        /// ## Formula
947        ///
948        /// ```text
949        /// reward = k / (x + eps)
950        /// ```
951        ///
952        /// ## Characteristics:
953        /// - **Inverse scaling**: Reward decreases as input increases
954        /// - **High initial rewards**: Small inputs yield larger rewards
955        /// - **Diminishing returns**: Larger inputs are progressively penalized
956        /// - **Stable near zero**: `eps` prevents singularities at x = 0
957        /// - **Deterministic**: Same input and parameters yield identical results
958        /// - **Fixed-point safe**: Uses fixed-point arithmetic for precision
959        /// - **Context-driven**: Controlled via [`InverseProportionalConfig`]
960        ///
961        /// ## Applications:
962        /// - Anti-whale incentive systems
963        /// - Rewarding early or small participants
964        /// - Load balancing and fairness mechanisms
965        /// - Resource pricing models
966        ///
967        /// ## Use Cases
968        ///
969        /// - Favoring smaller stakeholders over large ones
970        /// - Preventing concentration of rewards
971        /// - Incentivizing early-stage participation
972        ///
973        /// ## Example
974        ///
975        /// ```ignore
976        /// let config = InverseProportionalConfig {
977        ///     k: FixedU128::from_integer(100),
978        ///     epsilon: FixedU128::from_integer(1),
979        /// };
980        ///
981        /// let reward = InverseProportionalPayout::compute(9u128, Some(config));
982        /// // reward = 100 / (9 + 1) = 10
983        /// ```
984        name: pub InverseProportionalPayout,
985        input: Asset,
986        others: [FixedPoint],
987        context: InverseProportionalConfig<FixedPoint>,
988        bounds: [
989            Asset: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
990            FixedPoint: Copy + FixedPointNumber,
991        ],
992        compute: |input, context| {
993            let x = input.to_fixed();
994
995            // Compute denominator: x + eps
996            let denom = x.saturating_add(context.epsilon);
997
998            // non-positive denominator produces nonsensical or undefined
999            // results; return zero as a safe fallback.
1000            if denom <= FixedPoint::zero() {
1001                return Asset::from_fixed(&FixedPoint::zero());
1002            }
1003
1004            // Compute reward: k / (x + eps)
1005            let reward = context
1006                .k
1007                .checked_div(&denom)
1008                .unwrap_or_else(|| FixedPoint::zero());
1009
1010            Asset::from_fixed(&reward)
1011        }
1012    );
1013
1014    // ===============================================================================
1015    // ````````````````````````````` LOGARITHMIC-PAYOUT ``````````````````````````````
1016    // ===============================================================================
1017
1018    /// Defines the configuration for the [`LogarithmicPayout`] model.
1019    ///
1020    /// This struct specifies the parameters for a **logarithmic reward function**,
1021    /// enabling diminishing growth as input increases.
1022    ///
1023    /// **Concept**: **Logarithmic Growth Curve**
1024    ///
1025    /// Rewards follow a logarithmic function:
1026    ///
1027    /// ```text
1028    /// reward = a * ln(b * x + c) + d
1029    /// ```
1030    ///
1031    /// where:
1032    /// - `a` = vertical scaling (controls steepness)
1033    /// - `b` = horizontal scaling (input stretch/compression)
1034    /// - `c` = horizontal shift (ensures argument > 0)
1035    /// - `d` = vertical shift (baseline reward)
1036    /// - `x` = input variable (e.g., stake, time, or score)
1037    ///
1038    /// This model produces rapid initial growth that slows over time,
1039    /// resulting in diminishing returns for larger inputs.
1040    pub struct LogarithmicConfig<F>
1041    where
1042        F: FixedPointNumber,
1043    {
1044        /// Vertical scaling (a): controls steepness of growth
1045        pub vertical_scale: F,
1046
1047        /// Horizontal scaling (b): stretches/compresses input
1048        pub horizontal_scale: F,
1049
1050        /// Horizontal shift (c): ensures ln argument remains positive
1051        pub horizontal_shift: F,
1052
1053        /// Vertical shift (d): baseline reward offset
1054        pub vertical_shift: F,
1055    }
1056
1057    plugin_model! (
1058        /// The **LogarithmicPayout** model computes rewards using a **logarithmic function**,
1059        /// producing fast initial growth followed by diminishing returns.
1060        ///
1061        /// **Concept**: **Diminishing Growth Function**
1062        ///
1063        /// ## Formula
1064        ///
1065        /// ```text
1066        /// reward = a * ln(b * x + c) + d
1067        /// ```
1068        ///
1069        /// ## Characteristics:
1070        /// - **Diminishing returns**: Growth slows as input increases
1071        /// - **High early rewards**: Strong incentives for small inputs
1072        /// - **Unbounded (slow growth)**: Continues increasing but at decreasing rate
1073        /// - **Deterministic**: Same input and parameters yield identical results
1074        /// - **Fixed-point safe**: Uses fixed-point logarithmic operations
1075        /// - **Context-driven**: Controlled via [`LogarithmicConfig`]
1076        ///
1077        /// ## Applications:
1078        /// - Rewarding early participation more than late participation
1079        /// - Anti-whale incentive systems
1080        /// - Pricing curves (e.g., bonding curves)
1081        /// - Resource allocation with diminishing utility
1082        ///
1083        /// ## Use Cases
1084        ///
1085        /// - Incentivizing early adopters
1086        /// - Preventing exponential reward growth
1087        /// - Modeling diminishing marginal returns
1088        ///
1089        /// ## Example
1090        ///
1091        /// ```ignore
1092        /// let config = LogarithmicConfig {
1093        ///     vertical_scale: FixedU128::from_integer(10), // a
1094        ///     horizontal_scale: FixedU128::from_integer(1), // b
1095        ///     horizontal_shift: FixedU128::from_integer(1), // c
1096        ///     vertical_shift: FixedU128::from_integer(0),   // d
1097        /// };
1098        ///
1099        /// let reward = LogarithmicPayout::compute(9u128, Some(config));
1100        /// // reward = 10 * ln(9 + 1) ~= 10 * ln(10)
1101        /// ```
1102        name: pub LogarithmicPayout,
1103        input: Asset,
1104        others: [FixedPoint],
1105        context: LogarithmicConfig<FixedPoint>,
1106        bounds: [
1107            Asset: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
1108            FixedPoint: FixedPointNumber + FixedOp
1109        ],
1110        compute: |input, context| {
1111            let x = input.to_fixed();
1112
1113            // Compute inner term: (b * x + c)
1114            let inner = context
1115                .horizontal_scale
1116                .saturating_mul(x)
1117                .saturating_add(context.horizontal_shift);
1118
1119            // Compute ln(bx + c)
1120            let ln_val = FixedOp::fixed_ln(&inner).unwrap_or(FixedPoint::zero());
1121
1122            // Compute final reward: a * ln(...) + d
1123            let reward = context
1124                .vertical_scale
1125                .saturating_mul(ln_val)
1126                .saturating_add(context.vertical_shift);
1127
1128            Asset::from_fixed(&reward)
1129        }
1130    );
1131
1132    // ===============================================================================
1133    // `````````````````````````````` FIXED-RATE-PAYOUT ``````````````````````````````
1134    // ===============================================================================
1135
1136    /// Defines the configuration for the [`FixedRatePayout`] model.
1137    ///
1138    /// This struct specifies the parameters for a **fixed-rate reward function**,
1139    /// where rewards scale linearly with the input.
1140    ///
1141    /// **Concept**: **Proportional Scaling Function**
1142    ///
1143    /// Rewards follow a simple proportional relationship:
1144    ///
1145    /// ```text
1146    /// reward = x * r
1147    /// ```
1148    ///
1149    /// where:
1150    /// - `x` = input variable (e.g., stake, balance, or score)
1151    /// - `r` = fixed rate (fraction per unit input)
1152    ///
1153    /// This model produces rewards directly proportional to the input value.
1154    pub struct FixedRateConfig<F>
1155    where
1156        F: FixedPointNumber,
1157    {
1158        /// Fixed rate (r): percentage applied to input
1159        ///
1160        /// Example: `0.01` represents 1% of input
1161        pub rate: F,
1162    }
1163
1164    plugin_model!(
1165        /// The **FixedRatePayout** model computes rewards using a **fixed
1166        /// proportional rate**, scaling linearly with the input asset.
1167        ///
1168        /// **Concept**: **Linear Proportional Function**
1169        ///
1170        /// ## Formula
1171        ///
1172        /// ```text
1173        /// reward = x * r
1174        /// ```
1175        ///
1176        /// ## Characteristics:
1177        /// - **Linear scaling**: Reward increases proportionally with input
1178        /// - **Simple and efficient**: Minimal computation required
1179        /// - **Deterministic**: Same input and rate yield identical results
1180        /// - **Fixed-point safe**: Uses fixed-point arithmetic for precision
1181        /// - **Context-driven**: Controlled via [`FixedRateConfig`]
1182        ///
1183        /// ## Applications:
1184        /// - Percentage-based rewards (e.g., staking yield)
1185        /// - Fee or commission calculations
1186        /// - Proportional incentive distribution
1187        /// - Basic inflation mechanisms
1188        ///
1189        /// ## Use Cases
1190        ///
1191        /// - Rewarding participants based on stake size
1192        /// - Applying consistent percentage returns
1193        /// - Baseline reward models for systems
1194        ///
1195        /// ## Example
1196        ///
1197        /// ```ignore
1198        /// let config = FixedRateConfig {
1199        ///     rate: FixedU128::from_rational(1, 100), // 1%
1200        /// };
1201        ///
1202        /// let reward = FixedRatePayout::compute(1000u128, Some(config));
1203        /// // reward = 1000 * 0.01 = 10
1204        /// ```
1205        name: pub FixedRatePayout,
1206        input: Asset,
1207        others: [FixedPoint],
1208        context: FixedRateConfig<FixedPoint>,
1209        bounds: [
1210            Asset: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
1211            FixedPoint: FixedPointNumber
1212        ],
1213        compute: |input, context| {
1214            let x = input.to_fixed();
1215            let rate = context.rate;
1216
1217            // reward = x * r  (saturating to avoid overflow)
1218            let reward = x.saturating_mul(rate);
1219
1220            Asset::from_fixed(&reward)
1221        }
1222    );
1223
1224    // ===============================================================================
1225    // ````````````````````````````` FIXED-ANNUAL-PAYOUT `````````````````````````````
1226    // ===============================================================================
1227
1228    /// Defines the configuration for the [`FixedAnnualPayout`] model.
1229    ///
1230    /// This struct specifies the parameters for converting an **annual percentage
1231    /// rate (APR)** into a per-period reward.
1232    ///
1233    /// **Concept**: **Discrete Compounding Conversion**
1234    ///
1235    /// Rewards are derived by converting APR into an **effective per-period
1236    /// rate (EPR)**:
1237    ///
1238    /// ```text
1239    /// EPR = (1 + APR)^(1 / n) - 1
1240    /// reward = x * EPR
1241    /// ```
1242    ///
1243    /// where:
1244    /// - `APR` = annual percentage rate
1245    /// - `n`   = number of reward intervals per year
1246    /// - `x`   = input variable (e.g., stake or balance)
1247    ///
1248    /// This model ensures that rewards are distributed consistently across
1249    /// discrete time intervals while preserving the annual rate.
1250    pub struct FixedAnnualConfig<T, F>
1251    where
1252        F: FixedPointNumber,
1253    {
1254        /// Annual Percentage Rate (APR)
1255        pub apr: F,
1256
1257        /// Number of reward allocations per year (n)
1258        pub time_count: T,
1259    }
1260
1261    plugin_model!(
1262        /// The **FixedAnnualPayout** model computes rewards using a **per-period
1263        /// rate derived from an annual percentage rate (APR)**.
1264        ///
1265        /// **Concept**: **APR to Periodic Yield Conversion**
1266        ///
1267        /// ## Formula
1268        ///
1269        /// ```text
1270        /// EPR = (1 + APR)^(1 / n) - 1
1271        /// reward = x * EPR
1272        /// ```
1273        ///
1274        /// ## Characteristics:
1275        /// - **Time-aware scaling**: Converts annual rate into per-period rewards
1276        /// - **Compounding-consistent**: Preserves APR across discrete intervals
1277        /// - **Deterministic**: Same input and parameters yield identical results
1278        /// - **Fixed-point safe**: Uses fixed-point exponentiation
1279        /// - **Context-driven**: Controlled via [`FixedAnnualConfig`]
1280        ///
1281        /// ## Applications:
1282        /// - Staking reward systems with APR targets
1283        /// - Financial yield calculations
1284        /// - Periodic emission schedules
1285        /// - Interest-based incentive models
1286        ///
1287        /// ## Use Cases
1288        ///
1289        /// - Converting annual rewards into per-block or per-era payouts
1290        /// - Maintaining consistent yield across different time granularities
1291        /// - Financial modeling of compounding returns
1292        ///
1293        /// ## Example
1294        ///
1295        /// ```ignore
1296        /// let config = FixedAnnualConfig {
1297        ///     apr: FixedU128::from_rational(12, 100), // 12% APR
1298        ///     time_count: 12u128, // monthly payouts
1299        /// };
1300        ///
1301        /// let reward = FixedAnnualPayout::compute(1000u128, Some(config));
1302        /// // reward ~= 1000 * ((1.12)^(1/12) - 1)
1303        /// ```
1304        name: pub FixedAnnualPayout,
1305        input: Asset,
1306        others: [FixedPoint],
1307        context: FixedAnnualConfig<Asset, FixedPoint>,
1308        bounds: [
1309            Asset: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
1310            FixedPoint: FixedPointNumber + FixedOp
1311        ],
1312        compute: |input, context| {
1313            let one = FixedPoint::one();
1314 
1315            // guard against time_count == 0 (undefined period rate).
1316            let n = context.time_count.to_fixed();
1317            if n == FixedPoint::zero() {
1318                debug_assert!(false, "FixedAnnualPayout: time_count is zero - period rate undefined");
1319                return Asset::from_fixed(&FixedPoint::zero());
1320            }
1321 
1322            // Compute base: (1 + APR)
1323            let base = one.saturating_add(context.apr);
1324 
1325            // Compute exponent: 1 / n
1326            let exponent = one
1327                .checked_div(&n)
1328                .unwrap_or_else(|| FixedPoint::zero());
1329 
1330            // Compute (1 + APR)^(1/n).
1331            // on overflow (None), saturate toward max rather than zero.
1332            let power = match FixedOp::fixed_pow(&base, &exponent) {
1333                Some(v) => v,
1334                None => {
1335                    debug_assert!(false, "FixedAnnualPayout: fixed_pow overflowed - saturating to max");
1336                    FixedPoint::max_value()
1337                }
1338            };
1339 
1340            // EPR = (1 + APR)^(1/n) - 1
1341            let epr = power.saturating_sub(one);
1342 
1343            // reward = x * EPR
1344            let x = input.to_fixed();
1345            let reward = x.saturating_mul(epr);
1346 
1347            Asset::from_fixed(&reward)
1348        }
1349    );
1350
1351    // ===============================================================================
1352    // ``````````````````````````````` PIECEWISE-PAYOUT ``````````````````````````````
1353    // ===============================================================================
1354    
1355    /// Defines parameters for a **logistic (sigmoid) curve** used in piecewise reward modeling.
1356    ///
1357    /// **Concept**: **Logistic Curve Parameterization**
1358    ///
1359    /// The curve follows:
1360    ///
1361    /// ```text
1362    /// f(x) = L / (1 + e^(-k*(x - x0)))
1363    /// ```
1364    ///
1365    /// where:
1366    /// - `L`  = maximum value (upper asymptote)
1367    /// - `k`  = steepness (growth rate; positive for a growing S-curve)
1368    /// - `x0` = midpoint / inflection point
1369    ///
1370    /// ## Type Requirements
1371    ///
1372    /// `FixedPoint` must implement [`FixedSignedCast<Signed = FixedI128>`].
1373    /// Both unsigned and signed fixed-point types satisfy this bound. Signed
1374    /// arithmetic (`x - x0` when x < x0, and the negated exponent) is performed
1375    /// in the concrete `FixedI128` workspace and projected back.
1376    pub struct CurveParams<F>
1377    where
1378        F: FixedPointNumber,
1379    {
1380        /// Maximum reward level (L): upper asymptotic bound of the curve
1381        pub l: F,
1382    
1383        /// Steepness factor (k): controls how sharply the curve transitions.
1384        ///
1385        /// - k > 0: growing S-curve (output increases with x)
1386        /// - k < 0: decaying S-curve (output decreases with x)
1387        /// - k = 0: constant L/2 for all inputs
1388        pub k: F,
1389    
1390        /// Inflection point (x0): input value where the curve is steepest.
1391        ///
1392        /// At x = x0: f(x0) = L / 2.
1393        pub x0: F,
1394    }
1395    
1396    impl<FixedPoint> CurveParams<FixedPoint>
1397    where
1398        FixedPoint: FixedPointNumber + FixedSignedCast<Signed = FixedI128>,
1399    {
1400        /// Evaluates the logistic curve at input `x`.
1401        ///
1402        /// **Formula**
1403        ///
1404        /// ```text
1405        /// f(x) = L / (1 + e^(-k*(x - x0)))
1406        /// ```
1407        ///
1408        /// Signed intermediates (`x - x0`, `k*(x - x0)`, and its negation) are
1409        /// computed in `FixedI128` via [`FixedSignedCast`] so the method works
1410        /// correctly for both signed and unsigned `FixedPoint` types..
1411        pub fn evaluate(&self, x: FixedPoint) -> FixedPoint {
1412            let zero_s = FixedI128::zero();
1413    
1414            // Lift all operands into the signed workspace.
1415            // saturated_into is infallible for FixedU64 and clamps at i128::MAX
1416            // for large FixedU128 values; for signed types it is a zero-cost identity.
1417            let x_s:  FixedI128 = FixedSignedCast::saturated_into(x);
1418            let x0_s: FixedI128 = FixedSignedCast::saturated_into(self.x0);
1419            let k_s:  FixedI128 = FixedSignedCast::saturated_into(self.k);
1420            let l_s:  FixedI128 = FixedSignedCast::saturated_into(self.l);
1421    
1422            // Compute x - x0; negative for any x left of the inflection point.
1423            // The old code used unsigned saturating_sub, which clamped to 0 here
1424            // and made every point left of x0 behave as if x == x0.
1425            let diff_s: FixedI128 = x_s.saturating_sub(x0_s);
1426    
1427            // Compute -k*(x - x0).
1428            // k_s * diff_s is negative on the left half (diff_s < 0, k_s > 0)
1429            // and on the right half with a decaying curve (diff_s > 0, k_s < 0).
1430            // Negate via subtraction from zero - no .neg() on FixedPoint needed.
1431            let k_diff_s:     FixedI128 = k_s.saturating_mul(diff_s);
1432            let neg_k_diff_s: FixedI128 = zero_s.saturating_sub(k_diff_s);
1433    
1434            // Compute e^(-k*(x - x0)).
1435            // Concrete FixedI128::fixed_exp - handles negative arguments correctly.
1436            // On overflow use max_value() so the denominator is large and the
1437            // result approaches 0, which is the correct limit for a large exponent.
1438            let exp_s: FixedI128 = FixedI128::fixed_exp(&neg_k_diff_s)
1439                .unwrap_or(FixedI128::max_value());
1440    
1441            // Compute denominator: 1 + e^(...)
1442            let denom_s: FixedI128 = FixedI128::one().saturating_add(exp_s);
1443    
1444            // Final logistic value: L / denom
1445            let output_s: FixedI128 = l_s.checked_div(&denom_s).unwrap_or(zero_s);
1446    
1447
1448            // Project back to FixedPoint.
1449            // output_s >= 0 always (L >= 0, denom >= 1); saturated_from clamps
1450            // any accidental negative to 0 rather than panicking.
1451            FixedSignedCast::saturated_from(output_s)
1452        }
1453    }
1454    
1455    /// Defines a **piecewise segment** used in [`PiecewisePayout`].
1456    ///
1457    /// **Concept**: **Segmented Function Composition**
1458    ///
1459    /// Each segment defines a function over a specific input interval `[start_x, end_x]`.
1460    /// The overall payout function is constructed by combining multiple segments.
1461    pub enum Segment<F>
1462    where
1463        F: FixedPointNumber,
1464    {
1465        /// Linear segment using interpolation between two points.
1466        ///
1467        /// **Concept**: **Linear Interpolation**
1468        ///
1469        /// ```text
1470        /// f(x) = y1 + ((x - x1) / (x2 - x1)) * (y2 - y1)
1471        /// ```
1472        ///
1473        /// Both increasing (`y2 > y1`) and decreasing (`y2 < y1`) segments are
1474        /// supported for signed and unsigned types. The signed delta `y2 - y1`
1475        /// is computed in `FixedI128` via [`FixedSignedCast`].
1476        Linear {
1477            /// Starting input value (x1)
1478            start_x: F,
1479            /// Ending input value (x2)
1480            end_x: F,
1481            /// Output at start_x (y1)
1482            start_y: F,
1483            /// Output at end_x (y2)
1484            end_y: F,
1485        },
1486    
1487        /// Curve-based segment using a parameterized logistic function.
1488        ///
1489        /// **Concept**: **Parameterized Curve Segment**
1490        ///
1491        /// Uses [`CurveParams`] to evaluate:
1492        ///
1493        /// ```text
1494        /// f(x) = L / (1 + e^(-k*(x - x0)))
1495        /// ```
1496        ///
1497        /// Works correctly for both signed and unsigned `FixedPoint` types.
1498        Curve {
1499            /// Starting input value for the segment
1500            start_x: F,
1501            /// Ending input value for the segment
1502            end_x: F,
1503            /// Curve parameters defining shape and behavior
1504            params: CurveParams<F>,
1505        },
1506    }
1507    
1508    /// Defines the configuration for the [`PiecewisePayout`] model.
1509    ///
1510    /// **Concept**: **Piecewise Curve Composition**
1511    ///
1512    /// The reward function is:
1513    ///
1514    /// ```text
1515    /// reward(x) = f_i(x),  if x is in [x_i_start, x_i_end]
1516    ///             0,        if x matches no segment
1517    /// ```
1518    pub struct PiecewiseConfig<F>
1519    where
1520        F: FixedPointNumber,
1521    {
1522        /// Ordered list of segments defining the piecewise function.
1523        ///
1524        /// The first matching segment is used; segments may overlap without error.
1525        pub segments: Vec<Segment<F>>,
1526    }
1527    
1528    plugin_model!(
1529        /// The **PiecewisePayout** model computes rewards using a **piecewise-defined function**,
1530        /// selecting different behaviors depending on the input range.
1531        ///
1532        /// **Concept**: **Segmented Reward Function**
1533        ///
1534        /// ## Formula
1535        ///
1536        /// ```text
1537        /// reward(x) = f_i(x),  for the first segment i where x is in [start_x_i, end_x_i]
1538        ///             0,        if no segment matches
1539        /// ```
1540        ///
1541        /// ## Segment Types
1542        ///
1543        /// - **Linear**: `y = start_y + t * (end_y - start_y)` where `t = (x - x1) / (x2 - x1)`.
1544        ///   Supports both increasing and decreasing slopes for signed and unsigned types.
1545        /// - **Curve**: `y = L / (1 + e^(-k*(x - x0)))`.
1546        ///   Works correctly for unsigned types; signed arithmetic handled via `FixedI128`.
1547        ///
1548        /// ## Signed Arithmetic
1549        ///
1550        /// Two sub-computations require signed intermediates that unsigned fixed-point
1551        /// cannot represent:
1552        ///
1553        /// - **Curve segments**: `x - x0` is negative for `x < x0` (the left sigmoid tail).
1554        ///   The old code clamped this to 0 via unsigned `saturating_sub`, flattening the
1555        ///   entire left half of every curve segment.
1556        /// - **Decreasing linear segments**: `end_y - start_y` is negative when `end_y < start_y`.
1557        ///   The old code clamped to 0, turning every ramp-down into a flat line.
1558        ///
1559        /// Both are now computed in concrete `FixedI128` via [`FixedSignedCast`].
1560        ///
1561        /// ## Characteristics:
1562        /// - **Composable**: Combine Linear and Curve segments freely.
1563        /// - **Works with unsigned types**: No `Neg` bound, signed arithmetic in `FixedI128`.
1564        /// - **Deterministic**: Same input and configuration yield identical output.
1565        /// - **Context-driven**: Controlled via [`PiecewiseConfig`].
1566        ///
1567        /// ## Applications:
1568        /// - Multi-phase emission schedules.
1569        /// - Bootstrap, growth, and stabilization curves.
1570        /// - DeFi incentive design.
1571        /// - Tiered reward systems.
1572        ///
1573        /// ## Example
1574        ///
1575        /// ```ignore
1576        /// // Linear ramp: x in [0, 10] maps reward from 0 to 100.
1577        /// let config = PiecewiseConfig {
1578        ///     segments: vec![
1579        ///         Segment::Linear {
1580        ///             start_x: FixedU128::zero(),
1581        ///             end_x:   FixedU128::saturating_from_integer(10),
1582        ///             start_y: FixedU128::zero(),
1583        ///             end_y:   FixedU128::saturating_from_integer(100),
1584        ///         }
1585        ///     ],
1586        /// };
1587        /// assert_eq!(PiecewisePayout::compute(5u128, Some(config)), 50);
1588        /// ```
1589        name: pub PiecewisePayout,
1590        input: Asset,
1591        others: [FixedPoint],
1592        context: PiecewiseConfig<FixedPoint>,
1593        bounds: [
1594            Asset: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Zero,
1595            FixedPoint: FixedPointNumber + FixedSignedCast<Signed = FixedI128>,
1596        ],
1597        compute: |input, context| {
1598            let zero_fp = FixedPoint::zero();
1599            let _zero_s  = FixedI128::zero();
1600            let x: FixedPoint = input.to_fixed();
1601    
1602            // Find the first segment whose range contains x.
1603            // Segment bounds are stored as FixedPoint values; for unsigned types
1604            // they are always >= 0, and x (from a non-negative Asset) is also >= 0,
1605            // so the comparisons are correct without any signed-space conversion.
1606            let seg = context.segments.iter().find(|seg| match seg {
1607                Segment::Linear { start_x, end_x, .. } => x >= *start_x && x <= *end_x,
1608                Segment::Curve  { start_x, end_x, .. } => x >= *start_x && x <= *end_x,
1609            });
1610    
1611            let Some(segment) = seg else {
1612                return Asset::zero();
1613            };
1614    
1615            match segment {
1616                Segment::Linear { start_x, end_x, start_y, end_y } => {
1617                    let width = end_x.saturating_sub(*start_x);
1618    
1619                    // Degenerate segment (zero width): return start_y unchanged.
1620                    if width == zero_fp {
1621                        return Asset::from_fixed(start_y);
1622                    }
1623    
1624                    // Compute t = (x - start_x) / (end_x - start_x), always in [0, 1].
1625                    // x >= start_x is guaranteed by the segment match above, so
1626                    // saturating_sub is safe in unsigned space here.
1627                    let t: FixedPoint = x
1628                        .saturating_sub(*start_x)
1629                        .checked_div(&width)
1630                        .unwrap_or(zero_fp);
1631    
1632                    // Compute delta_y = end_y - start_y in signed space.
1633                    // This value is negative for any decreasing segment (end_y < start_y).
1634                    // The old code used end_y.saturating_sub(start_y), which clamped
1635                    // to 0 for unsigned FixedPoint, turning every ramp-down into a
1636                    // flat line at start_y.
1637                    let start_y_s: FixedI128 = FixedSignedCast::saturated_into(*start_y);
1638                    let end_y_s:   FixedI128 = FixedSignedCast::saturated_into(*end_y);
1639                    let delta_y_s: FixedI128 = end_y_s.saturating_sub(start_y_s); // may be < 0
1640    
1641                    // t is in [0, 1] and non-negative; promote for the multiply.
1642                    let t_s: FixedI128 = FixedSignedCast::saturated_into(t);
1643                    // Compute y = start_y + t * delta_y
1644                    let y_s: FixedI128 = start_y_s.saturating_add(t_s.saturating_mul(delta_y_s));
1645    
1646                    // Project back to FixedPoint.
1647                    // For unsigned types, a negative y (possible when delta_y < 0 and
1648                    // t > 0) clamps to 0 via saturated_from, which is the correct
1649                    // saturating semantic for rewards.
1650                    let y_fp: FixedPoint = FixedSignedCast::saturated_from(y_s);
1651                    Asset::from_fixed(&y_fp)
1652                }
1653    
1654                Segment::Curve { params, .. } => {
1655                    // Delegate to CurveParams::evaluate, which performs all signed
1656                    // arithmetic in FixedI128 via FixedSignedCast.
1657                    let y: FixedPoint = params.evaluate(x);
1658                    Asset::from_fixed(&y)
1659                }
1660            }
1661        }
1662    );
1663
1664}
1665
1666pub use payee::*;
1667
1668/// Defines **pluggable payee models** for distributing a total payout
1669/// across a set of participants.
1670///
1671/// Payees are abstracted as allocation models that transform a total payout
1672/// and a set of participant weights into **individual reward assignments**.
1673///
1674/// ## Concept
1675///
1676/// - A payee model determines **how a total payout is distributed**.
1677/// - The distribution is computed over a set of `(Id, Share)` pairs.
1678/// - The output assigns a payout value to each participant.
1679///
1680/// ## Mathematical Form
1681///
1682/// Where:
1683/// - `P` = total payout
1684/// - `p_i` = payout assigned to participant `i`
1685/// - `n` = number of participants
1686///
1687/// ## In this model:
1688///
1689/// - Input is `(Payout, [(Id, Share)])`.
1690/// - Output is `[(Id, Payout)]`.
1691///
1692/// ## Properties
1693///
1694/// - **Conservative distribution**: Total assigned payout equals input payout.
1695/// - **Deterministic mapping**: Same inputs produce identical allocations.
1696/// - **Context-free or context-driven** depending on model.
1697/// - **Finite partitioning** of total value across participants.
1698///
1699/// ## Purpose
1700///
1701/// Payee models provide flexibility in defining reward distribution:
1702///
1703/// - Control how total rewards are allocated among participants.
1704/// - Support different allocation strategies based on weights or structure.
1705/// - Serve as the final stage in reward pipelines.
1706pub mod payee {
1707
1708    // ===============================================================================
1709    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
1710    // ===============================================================================
1711
1712    // --- Core / Std ---
1713    use core::iter::once;
1714
1715    // --- FRAME Suite ---
1716    use frame_suite::{
1717        fixedpoint::{FixedForInteger, FixedOp, IntegerToFixed},
1718        plugin_model,
1719    };
1720
1721    // --- Substrate primitives ---
1722    use sp_runtime::{
1723        traits::{One, Saturating, Zero},
1724        FixedPointNumber, Vec,
1725    };
1726
1727    // ===============================================================================
1728    // ````````````````````````````````` SHARES PAYEE ````````````````````````````````
1729    // ===============================================================================
1730
1731    plugin_model!(
1732        /// The **SharesPay** model distributes a total payout proportionally
1733        /// based on participant shares.
1734        ///
1735        /// **Concept**: **Proportional Allocation with Remainder Correction**
1736        ///
1737        /// ## Mathematical Form
1738        ///
1739        /// `p_i = floor(P * s_i / sum(s_j))`
1740        ///
1741        /// Subject to:
1742        ///
1743        /// `sum(p_i) = P`
1744        ///
1745        /// Where:
1746        /// - `P` = total payout
1747        /// - `s_i` = share of participant `i`
1748        /// - `p_i` = payout assigned to participant `i`
1749        /// - `n` = number of participants
1750        ///
1751        /// ## Properties
1752        ///
1753        /// - **Proportional allocation**: Each payout is derived from relative share.
1754        /// - **Discrete rounding**: Values are floored to integer representation.
1755        /// - **Remainder redistribution**: Residual units are reassigned to preserve
1756        ///   total sum.
1757        /// - **Conservative**: Total distributed payout equals input payout.
1758        /// - **Deterministic**: Same inputs produce identical allocation.
1759        ///
1760        /// ## Purpose
1761        ///
1762        /// - Enables share-based distribution of rewards.
1763        /// - Preserves proportional fairness under discrete constraints.
1764        /// - Ensures exact conservation of total payout.
1765        name: pub SharesPay,
1766        input: (Payout,PayoutFor),
1767        output: Payees,
1768        others: [Id, Share, FixedPoint],
1769        bounds: [
1770            Payout: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Saturating + Copy + Zero + PartialOrd,
1771        PayoutFor: IntoIterator<Item = (Id, Share)> + Clone + FromIterator<(Id, Share)>,
1772        Share: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Zero + Saturating,
1773        Payees: Extend<(Id, Payout)> + Default,
1774        Id: Clone,
1775        FixedPoint: FixedPointNumber + FixedOp
1776    ],
1777    compute: |input, _context| {
1778        let (payout, payout_for) = input;
1779        let payout_fixed = payout.to_fixed();
1780
1781        let (total_shares, len) = payout_for
1782            .clone()
1783            .into_iter()
1784            .fold((Share::zero(), 0usize), |(acc_share, acc_len), (_, share)| {
1785                (acc_share.saturating_add(share), acc_len.saturating_add(1usize))
1786            });
1787
1788        let mut payees = Payees::default();
1789
1790        if total_shares.is_zero() || len.is_zero() {
1791            return payees
1792        }
1793
1794        // Calculate each payee's raw payout (fixed-point), floor it, and track the remainder
1795        let mut payouts: Vec<(Id, Payout, FixedPoint)> = Vec::with_capacity(len);
1796        let mut total_distributed: Payout = Payout::zero();
1797        let mut remainders: Vec<(usize, FixedPoint)> = Vec::with_capacity(len);
1798
1799        for (idx, (id, share)) in payout_for.clone().into_iter().enumerate() {
1800            let ratio = share.to_fixed().checked_div(&total_shares.to_fixed()).unwrap_or_else(|| Share::zero().to_fixed());
1801            let pay_fp = payout_fixed.saturating_mul(ratio);
1802            let pay_int = Payout::from_fixed(&pay_fp);
1803            let pay_fp_int = pay_int.to_fixed();
1804            let remainder = pay_fp.saturating_sub(pay_fp_int);
1805            payouts.push((id.clone(), pay_int, remainder));
1806            total_distributed = total_distributed.saturating_add(pay_int);
1807            remainders.push((idx, remainder));
1808        }
1809
1810        // Calculate how much is left undistributed due to flooring
1811        let mut undistributed = payout.saturating_sub(total_distributed);
1812
1813        // Sort remainders descending, distribute +1 to top N
1814        remainders.sort_by(|a, b| b.1.cmp(&a.1));
1815        let mut i = 0;
1816        while undistributed > Payout::zero() && i < remainders.len() {
1817            let idx = remainders[i].0;
1818            payouts[idx].1 = payouts[idx].1.saturating_add(Payout::from_fixed(&FixedPoint::one()));
1819            undistributed = undistributed.saturating_sub(Payout::from_fixed(&FixedPoint::one()));
1820            i += 1;
1821        }
1822
1823        for (id, pay, _) in payouts {
1824            payees.extend(core::iter::once((id, pay)));
1825        }
1826
1827        payees
1828    }
1829
1830    );
1831
1832    // ===============================================================================
1833    // ````````````````````````````````` EQUAL PAYEE `````````````````````````````````
1834    // ===============================================================================
1835
1836    plugin_model!(
1837        /// The **EqualPay** model distributes a total payout equally
1838        /// among all participants.
1839        ///
1840        /// **Concept**: **Uniform Allocation**
1841        ///
1842        /// ## Mathematical Form
1843        ///
1844        /// `p_i = floor( P / n )`
1845        ///
1846        /// Subject to:
1847        ///
1848        /// `sum(p_i) <= P`
1849        ///
1850        /// Where:
1851        /// - `P` = total payout
1852        /// - `p_i` = payout assigned to participant `i`
1853        /// - `n` = number of participants
1854        ///
1855        /// ## Properties
1856        ///
1857        /// - **Uniform allocation**: Each participant receives the same payout.
1858        /// - **Discrete division**: Values are derived via integer division.
1859        /// - **Non-exhaustive**: Total distributed payout may be less than input due to rounding.
1860        /// - **Deterministic**: Same inputs produce identical allocation.
1861        ///
1862        /// ## Purpose
1863        ///
1864        /// - Enables equal distribution of rewards.
1865        /// - Provides simple and uniform allocation strategy.
1866        /// - Suitable when all participants are treated identically.
1867        name: pub EqualPay,
1868        input: (Payout,PayoutFor),
1869        output: Payees,
1870        others: [Id, Share, FixedPoint],
1871        bounds: [
1872            Payout: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Saturating + Zero + One,
1873            PayoutFor: IntoIterator<Item = (Id, Share)> + Clone + FromIterator<(Id, Share)>,
1874            Share: Copy + IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint>,
1875            Payees: Extend<(Id, Payout)> + Default,
1876            Id: Clone,
1877            FixedPoint: FixedPointNumber + FixedOp
1878        ],
1879        compute: |input, _context| {
1880        let (payout, payout_for) = input;
1881            let payout_fixed = payout.to_fixed();
1882            let mut payees = Payees::default();
1883
1884            // Count the number of payees
1885            let count: usize = payout_for.clone().into_iter().count();
1886
1887            if count == 0 {
1888                return payees
1889            }
1890
1891            // Build count as FixedPoint by adding One repeatedly
1892            let mut count_fixed = FixedPoint::one();
1893            for _ in 1..count {
1894                count_fixed = count_fixed.saturating_add(FixedPoint::one());
1895            }
1896
1897            // Divide payout equally among all payees
1898            let equal_pay = payout_fixed.checked_div(&count_fixed)
1899                .unwrap_or_else(|| FixedPoint::zero());
1900
1901            for (id, _) in payout_for.clone() {
1902                payees.extend(once((
1903                    id.clone(),
1904                    Payout::from_fixed(&equal_pay)
1905                )));
1906            }
1907            payees
1908        }
1909    );
1910}
1911
1912// ===============================================================================
1913// ````````````````````` PAYOUT & PAYEE MODELS PLUGIN TESTS ``````````````````````
1914// ===============================================================================
1915
1916#[cfg(test)]
1917mod tests {
1918
1919    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1920    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
1921    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1922
1923    // --- Local crate imports ---
1924    use super::*;
1925    use crate::rewards::payout::{CurveParams, Segment};
1926
1927    // --- FRAME Suite ---
1928    use frame_suite::plugin_test;
1929
1930    // --- Substrate primitives ---
1931    use sp_runtime::{AccountId32, FixedI128, FixedPointNumber, FixedU128, traits::{One, Zero}};
1932
1933    // --- Substrate std (no_std helpers) ---
1934    use sp_std::vec;
1935
1936    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1937    // `````````````````````````````````` CONSTANTS ``````````````````````````````````
1938    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1939
1940    const fn account_frm_seed(seed: u8) -> AccountId32 {
1941        let mut data = [0u8; 32];
1942        data[31] = seed;
1943        AccountId32::new(data)
1944    }
1945
1946    const ALICE: AccountId32 = account_frm_seed(1);
1947    const BOB: AccountId32 = account_frm_seed(2);
1948    const MIKE: AccountId32 = account_frm_seed(5);
1949
1950    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1951    // ```````````````````````````````` PAYOUT MODELS ````````````````````````````````
1952    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1953
1954    // ---------------------------------- ZERO-PAYOUT ---------------------------------
1955
1956    plugin_test! {
1957        model: payout::ZeroPayout,
1958        input: u128,
1959        cases: {
1960        (zero_payout_returns_zero_for_zero_input, 0, 0),
1961        (zero_payout_returns_zero_for_positive_input, 1000, 0),
1962        (zero_payout_returns_zero_for_random_input, 15755, 0),
1963        (zero_payout_returns_zero_for_max_input, u128::MAX, 0),
1964        }
1965    }
1966
1967    // ---------------------------------- CONSTANT-PAYOUT ---------------------------------
1968
1969    plugin_test! {
1970        model: payout::ConstantPayout,
1971        input: u128,
1972        output: u128,
1973        context: payout::ConstantPayoutConfig<u128>,
1974        value: payout::ConstantPayoutConfig { payout: 500 },
1975        cases: {
1976        (constant_payout_always_returns_init_reward_for_zero, 0, 500),
1977        (constant_payout_always_returns_init_reward_for_positive, 1000, 500),
1978        (constant_payout_always_returns_init_reward_for_random, 45200, 500),
1979        (constant_payout_always_returns_init_reward_for_max, u128::MAX, 500),
1980        }
1981    }
1982
1983    // ---------------------------------- INFLATION-PAYOUT ---------------------------------
1984
1985    plugin_test! {
1986        model: payout::InflationPayout,
1987        input: u128,
1988        output: u128,
1989        context: payout::InflationPayoutConfig<FixedU128>,
1990        value: payout::InflationPayoutConfig {
1991            inflation_rate: FixedU128::saturating_from_rational(1, 10), // 10%
1992        },
1993        cases: {
1994        (inflation_payout_10_percent_of_1000, 1000, 100),
1995        (inflation_payout_10_percent_of_500, 500, 50),
1996        (inflation_payout_10_percent_of_10, 10, 1),
1997        }
1998    }
1999
2000    plugin_test! {
2001        model: payout::InflationPayout,
2002        input: i128,
2003        output: i128,
2004        context: payout::InflationPayoutConfig<FixedI128>,
2005        value: payout::InflationPayoutConfig {
2006            inflation_rate: FixedI128::saturating_from_rational(1, 10), // 10%
2007        },
2008        cases: {
2009        (inflation_payout_signed_10_percent_of_1000, -1000, -100),
2010        (inflation_payout_signed_10_percent_of_500, -500, -50),
2011        (inflation_payout_signed_10_percent_of_10, -10, -1),
2012        }
2013    }
2014
2015    // ---------------------------------- LINEAR-PAYOUT ---------------------------------
2016
2017    plugin_test! {
2018        model: payout::LinearPayout,
2019        input: u128,
2020        output: u128,
2021        context: payout::LinearPayoutConfig<FixedU128>,
2022        value: payout::LinearPayoutConfig {
2023            slope: FixedU128::saturating_from_integer(2),
2024            base_reward: FixedU128::saturating_from_integer(10),
2025        },
2026        cases: {
2027        (linear_payout_2x_plus_10_for_5, 5, 20), // 2*5 + 10
2028        (linear_payout_2x_plus_10_for_0, 0, 10),
2029        (linear_payout_2x_plus_10_for_150, 150, 310),
2030        (linear_payout_2x_plus_10_for_725, 725, 1460),
2031        }
2032    }
2033
2034    plugin_test! {
2035        model: payout::LinearPayout,
2036        input: i128,
2037        output: i128,
2038        context: payout::LinearPayoutConfig<FixedI128>,
2039        value: payout::LinearPayoutConfig {
2040            slope: FixedI128::saturating_from_integer(2),
2041            base_reward: FixedI128::saturating_from_integer(10),
2042        },
2043        cases: {
2044        (linear_payout_signed_case_1, -5, 0), // 2*-5 + 10
2045        (linear_payout_signed_case_2, -2, 6),
2046        (linear_payout_signed_case_3, -150, -290),
2047        (linear_payout_signed_case_4, -725, -1440),
2048        }
2049    }
2050 
2051    // ---------------------------------- QUADRATIC-PAYOUT ---------------------------------
2052
2053    plugin_test! {
2054        model: payout::QuadraticPayout,
2055        input: u128,
2056        output: u128,
2057        context: payout::QuadraticPayoutConfig<FixedU128>,
2058        value: payout::QuadraticPayoutConfig {
2059            quadratic_coeff: FixedU128::saturating_from_integer(1),
2060            linear_coeff: FixedU128::saturating_from_integer(2),
2061            constant_term: FixedU128::saturating_from_integer(3),
2062        },
2063        cases: {
2064        (quadratic_payout_x2_plus_2x_plus_3_for_2, 2, 11), // 1*4 + 2*2 + 3
2065        (quadratic_payout_x2_plus_2x_plus_3_for_0, 0, 3),
2066        (quadratic_payout_x2_plus_2x_plus_3_for_125, 125, 15878),
2067        (quadratic_payout_x2_plus_2x_plus_3_for_2675, 2675, 7160978),
2068        }
2069    }
2070
2071    plugin_test! {
2072        model: payout::QuadraticPayout,
2073        input: i128,
2074        output: i128,
2075        context: payout::QuadraticPayoutConfig<FixedI128>,
2076        value: payout::QuadraticPayoutConfig {
2077            quadratic_coeff: FixedI128::saturating_from_integer(1),
2078            linear_coeff: FixedI128::saturating_from_integer(2),
2079            constant_term: FixedI128::saturating_from_integer(3),
2080        },
2081        cases: {
2082        (quadratic_payout_signed_case_1, -2, 3), // 1*4 + 2*-2 + 3
2083        (quadratic_payout_signed_case_2, -24, 531),
2084        (quadratic_payout_signed_case_3, -125, 15378),
2085        (quadratic_payout_signed_case_4, -2675, 7150278),
2086        }
2087    }
2088
2089    // ---------------------------------- HALVING-PAYOUT ---------------------------------
2090    
2091    plugin_test! {
2092        model: payout::HalvingPayout,
2093        input: u128,
2094        output: u128,
2095        context: payout::HalvingPayoutConfig<u128>,
2096        value: payout::HalvingPayoutConfig { initial_reward: 1024 },
2097        cases: {
2098        (halving_payout_initial_reward_for_era_0, 0, 1024),
2099        (halving_payout_halved_for_era_1, 1, 512),
2100        (halving_payout_halved_for_era_2, 2, 256),
2101        (halving_payout_halved_for_era_8, 8, 4),
2102        (halving_payout_halved_for_era_10, 10, 1),
2103        (halving_payout_zero_for_era_11, 11, 0)
2104        }
2105    }
2106 
2107    plugin_test! {
2108        model: payout::HalvingPayout,
2109        input: i128,
2110        output: i128,
2111        context: payout::HalvingPayoutConfig<i128>,
2112        value: payout::HalvingPayoutConfig { initial_reward: -1024 },
2113        cases: {
2114        (halving_payout_negative_initial_for_era_0, 0, -1024),
2115        (halving_payout_negative_halved_for_era_1, 1, -512),
2116        (halving_payout_negative_halved_for_era_2, 2, -256),
2117        (halving_payout_negative_halved_for_era_6, 6, -16),
2118        }
2119    }
2120
2121    // ---------------------------------- EXP-DECAY-PAYOUT ----------------------------------
2122
2123    // A -> UNSIGNED ASSET
2124
2125    // --- A1. Standard decay: a=0.1, r0=1000 ---
2126    plugin_test! {
2127        model: ExpDecayPayout,
2128        input: u128,
2129        output: u128,
2130        context: ExpDecayPayoutConfig<u128, FixedU128>,
2131        value: ExpDecayPayoutConfig {
2132            initial_reward: 1000u128,
2133            decay_constant: FixedU128::saturating_from_rational(1, 10), // a = 0.1
2134        },
2135        cases: {
2136        // x=0: e^0 = 1.0 -> reward = 1000
2137        (exp_decay_unsigned_x0, 0, 1000),
2138        // x=1: e^-0.1 ~= 0.9048 -> 904
2139        (exp_decay_unsigned_x1, 1, 904),
2140        // x=5: e^-0.5 ~= 0.6065 -> 606
2141        (exp_decay_unsigned_x5, 5, 606),
2142        // x=10: e^-1.0 ~= 0.3679 -> 367
2143        (exp_decay_unsigned_x10, 10, 367),
2144        // x=20: e^-2.0 ~= 0.1353 -> 135
2145        (exp_decay_unsigned_x20, 20, 135),
2146        // x=50: e^-5.0 ~= 0.0067 -> 6
2147        (exp_decay_unsigned_x50, 50, 6),
2148        // x=100: e^-10 ~= 0.000045 -> 0 (truncated)
2149        (exp_decay_unsigned_x100, 100, 0),
2150        }
2151    }
2152
2153    // --- A2. Zero decay constant (a=0): reward is constant at r0 for all x ---
2154    //
2155    // e^(-0 * x) = e^0 = 1, so reward = r0 regardless of x.
2156    plugin_test! {
2157        model: ExpDecayPayout,
2158        input: u128,
2159        output: u128,
2160        context: ExpDecayPayoutConfig<u128, FixedU128>,
2161        value: ExpDecayPayoutConfig {
2162            initial_reward: 500u128,
2163            decay_constant: FixedU128::zero(), // a = 0
2164        },
2165        cases: {
2166            (exp_decay_unsigned_zero_decay_x0,   0, 500),
2167            (exp_decay_unsigned_zero_decay_x10,  10, 500),
2168            (exp_decay_unsigned_zero_decay_x100, 100, 500),
2169        }
2170    } 
2171
2172    // --- A3. Zero initial reward (r0=0): always 0 regardless of decay ---
2173    //
2174    // 0 * anything = 0.
2175    plugin_test! {
2176        model: ExpDecayPayout,
2177        input: u128,
2178        output: u128,
2179        context: ExpDecayPayoutConfig<u128, FixedU128>,
2180        value: ExpDecayPayoutConfig {
2181            initial_reward: 0u128,
2182            decay_constant: FixedU128::saturating_from_rational(1, 10),
2183        },
2184        cases: {
2185            (exp_decay_unsigned_zero_r0_x0,  0, 0),
2186            (exp_decay_unsigned_zero_r0_x10, 10, 0),
2187        }
2188    }
2189
2190    // --- A4. a=1 (fast decay), r0=100 ---
2191    //
2192    // x=0 -> 100, x=1 -> ~36, x=2 -> ~13, x=3 -> ~4
2193    plugin_test! {
2194        model: ExpDecayPayout,
2195        input: u128,
2196        output: u128,
2197        context: ExpDecayPayoutConfig<u128, FixedU128>,
2198        value: ExpDecayPayoutConfig {
2199            initial_reward: 100u128,
2200            decay_constant: FixedU128::one(), // a = 1
2201        },
2202        cases: {
2203            (exp_decay_unsigned_fast_x0, 0, 100),
2204            (exp_decay_unsigned_fast_x1, 1, 36),
2205            (exp_decay_unsigned_fast_x2, 2, 13),
2206            (exp_decay_unsigned_fast_x3, 3, 4),
2207            (exp_decay_unsigned_fast_x4, 4, 1),
2208            // x=5: e^-5 ~= 0.0067 -> 0 (truncated to integer)
2209            (exp_decay_unsigned_fast_x5, 5, 0),
2210        }
2211    }
2212
2213    // --- A5. a=ln(2)~=0.693, r0=1024: each unit step halves the reward ---
2214    //
2215    // This is the continuous analog of HalvingPayout. Because ln(2) is
2216    // irrational and we use a rational approximation (693/1000), results
2217    // track 512, 256, 128, ... with small rounding errors.
2218    plugin_test! {
2219        model: ExpDecayPayout,
2220        input: u128,
2221        output: u128,
2222        context: ExpDecayPayoutConfig<u128, FixedU128>,
2223        value: ExpDecayPayoutConfig {
2224            initial_reward: 1024u128,
2225            // ln(2) ~= 0.6931471... ~= 693/1000 (good enough for 4 halvings)
2226            decay_constant: FixedU128::saturating_from_rational(693, 1000),
2227        },
2228        cases: {
2229            // x=0: 1024 * e^0 = 1024
2230            (exp_decay_unsigned_halving_x0, 0, 1024),
2231            // x=1: 1024 * e^-ln2 = 512 (tiny rounding -> 512)
2232            (exp_decay_unsigned_halving_x1, 1, 512),
2233            // x=2: 1024 * e^-2ln2 = 256
2234            (exp_decay_unsigned_halving_x2, 2, 256),
2235            // x=3: 1024 * e^-3ln2 = 128
2236            (exp_decay_unsigned_halving_x3, 3, 128),
2237        }
2238    }
2239 
2240    // --- A6. Large x causes exp to approach zero - should not panic ---
2241    plugin_test! {
2242        model: ExpDecayPayout,
2243        input: u128,
2244        output: u128,
2245        context: ExpDecayPayoutConfig<u128, FixedU128>,
2246        value: ExpDecayPayoutConfig {
2247            initial_reward: 1_000_000u128,
2248            decay_constant: FixedU128::one(), // a = 1
2249        },
2250        cases: {
2251            // e^-50 is astronomically small - truncates to 0
2252            (exp_decay_unsigned_large_x, 50, 0),
2253        }
2254    }
2255
2256    // B -> SIGNED ASSET
2257 
2258    // --- B1. Standard decay: a=0.1, r0=1000 ---
2259    plugin_test! {
2260        model: ExpDecayPayout,
2261        input: i128,
2262        output: i128,
2263        context: ExpDecayPayoutConfig<i128, FixedI128>,
2264        value: ExpDecayPayoutConfig {
2265            initial_reward: 1000i128,
2266            decay_constant: FixedI128::saturating_from_rational(1, 10),
2267        },
2268        cases: {
2269            (exp_decay_signed_x0,   0, 1000),
2270            (exp_decay_signed_x1,   1, 904),
2271            (exp_decay_signed_x5,   5, 606),
2272            (exp_decay_signed_x10,  10, 367),
2273            (exp_decay_signed_x20,  20, 135),
2274            (exp_decay_signed_x50,  50, 6),
2275            (exp_decay_signed_x100, 100, 0),
2276        }
2277    }
2278 
2279    // --- B2. Negative input (x < 0): -a*x becomes positive -> e^(positive) > 1 ---
2280    plugin_test! {
2281        model: ExpDecayPayout,
2282        input: i128,
2283        output: i128,
2284        context: ExpDecayPayoutConfig<i128, FixedI128>,
2285        value: ExpDecayPayoutConfig {
2286            initial_reward: 1000i128,
2287            decay_constant: FixedI128::saturating_from_rational(1, 10),
2288        },
2289        cases: {
2290            // x=-10: e^(+1.0) ~= 2.718 -> reward = 2718
2291            (exp_decay_signed_neg_x10, -10, 2718),
2292            // x=-5: e^(+0.5) ~= 1.6487 -> reward = 1648
2293            (exp_decay_signed_neg_x5, -5, 1648),
2294            // x=-1: e^(+0.1) ~= 1.1052 -> reward = 1105
2295            (exp_decay_signed_neg_x1, -1, 1105),
2296        }
2297    }
2298 
2299    // --- B3. Zero decay, signed ---
2300    plugin_test! {
2301        model: ExpDecayPayout,
2302        input: i128,
2303        output: i128,
2304        context: ExpDecayPayoutConfig<i128, FixedI128>,
2305        value: ExpDecayPayoutConfig {
2306            initial_reward: 500i128,
2307            decay_constant: FixedI128::zero(),
2308        },
2309        cases: {
2310            (exp_decay_signed_zero_decay_x0,  0, 500),
2311            (exp_decay_signed_zero_decay_x10, 10, 500),
2312        }
2313    }
2314 
2315    // --- B4. Zero initial reward, signed ---
2316    plugin_test! {
2317        model: ExpDecayPayout,
2318        input: i128,
2319        output: i128,
2320        context: ExpDecayPayoutConfig<i128, FixedI128>,
2321        value: ExpDecayPayoutConfig {
2322            initial_reward: 0i128,
2323            decay_constant: FixedI128::saturating_from_rational(1, 10),
2324        },
2325        cases: {
2326            (exp_decay_signed_zero_r0_x0,  0, 0),
2327            (exp_decay_signed_zero_r0_x10, 10, 0),
2328        }
2329    }
2330 
2331    // --- B5. Fast decay (a=1), signed ---
2332    plugin_test! {
2333        model: ExpDecayPayout,
2334        input: i128,
2335        output: i128,
2336        context: ExpDecayPayoutConfig<i128, FixedI128>,
2337        value: ExpDecayPayoutConfig {
2338            initial_reward: 100i128,
2339            decay_constant: FixedI128::one(),
2340        },
2341        cases: {
2342            (exp_decay_signed_fast_x0, 0, 100),
2343            (exp_decay_signed_fast_x1, 1, 36),
2344            (exp_decay_signed_fast_x2, 2, 13),
2345            (exp_decay_signed_fast_x3, 3, 4),
2346            (exp_decay_signed_fast_x5, 5, 0),
2347        }
2348    }
2349
2350    // ----------------------------------- SIGMOID-PAYOUT -----------------------------------
2351
2352    // A -> UNSIGNED ASSET
2353
2354    // --- A1. Standard curve: a=0.1, b=0.9, L=100 ---
2355    plugin_test! {
2356        model: SigmoidPayout,
2357        input: u128,
2358        output: u128,
2359        context: SigmoidPayoutConfig<u128, FixedU128>,
2360        value: SigmoidPayoutConfig {
2361            max_reward: 100u128,
2362            growth_start: FixedU128::saturating_from_rational(1, 10), // 0.1
2363            growth_end: FixedU128::saturating_from_rational(9, 10), // 0.9
2364        },
2365        cases: {
2366            // x=0: lower tail, f(0) = a*L = 10 (exact by construction)
2367            (sigmoid_unsigned_x0_lower_tail, 0, 10),
2368            // x=1: upper tail, f(1) = b*L = 90 (rounds to 89 due to fixed-point rounding)
2369            (sigmoid_unsigned_x1_upper_tail, 1, 89),
2370            // x=5: deep saturation -> 100
2371            (sigmoid_unsigned_x5_saturation, 5, 99),
2372            // x=100: fully saturated -> 100
2373            (sigmoid_unsigned_x100_full_saturation, 100, 100),
2374        }
2375    }    
2376
2377    // --- A2. Wide curve: a=0.2, b=0.8, L=1000 ---
2378    plugin_test! {
2379        model: SigmoidPayout,
2380        input: u128,
2381        output: u128,
2382        context: SigmoidPayoutConfig<u128, FixedU128>,
2383        value: SigmoidPayoutConfig {
2384            max_reward: 1000u128,
2385            growth_start: FixedU128::saturating_from_rational(2, 10), // 0.2
2386            growth_end: FixedU128::saturating_from_rational(8, 10), // 0.8
2387        },
2388        cases: {
2389            (sigmoid_unsigned_wide_x0, 0, 200),
2390            (sigmoid_unsigned_wide_x1, 1, 799),
2391            (sigmoid_unsigned_wide_x10, 10, 999),
2392        }
2393    }
2394 
2395    // --- A3. Very steep curve: a=0.01, b=0.99, L=100 ---
2396    plugin_test! {
2397        model: SigmoidPayout,
2398        input: u128,
2399        output: u128,
2400        context: SigmoidPayoutConfig<u128, FixedU128>,
2401        value: SigmoidPayoutConfig {
2402            max_reward: 100u128,
2403            growth_start: FixedU128::saturating_from_rational(1, 100), // 0.01
2404            growth_end: FixedU128::saturating_from_rational(99, 100), // 0.99
2405        },
2406        cases: {
2407            // x=0: f(0) = 1*L/100 = 1
2408            (sigmoid_unsigned_steep_x0, 0, 1),
2409            // x=1: f(1) ~= 99 (rounding -> 98)
2410            (sigmoid_unsigned_steep_x1, 1, 98),
2411            // x=5: saturation
2412            (sigmoid_unsigned_steep_x5, 5, 100),
2413        }
2414    }
2415 
2416    // --- A4. a = 0.5 (midpoint at x=0): symmetric about x=0 ---
2417    plugin_test! {
2418        model: SigmoidPayout,
2419        input: u128,
2420        output: u128,
2421        context: SigmoidPayoutConfig<u128, FixedU128>,
2422        value: SigmoidPayoutConfig {
2423            max_reward: 100u128,
2424            growth_start: FixedU128::saturating_from_rational(1, 2), // 0.5 -> logit=0 -> x0=0
2425            growth_end: FixedU128::saturating_from_rational(9, 10), // 0.9
2426        },
2427        cases: {
2428            // f(0) = L/2 = 50 (exact - x0=0 so x=0 is the midpoint)
2429            (sigmoid_unsigned_midpoint_at_x0, 0,  50),
2430            // f(1) ~= b*L = 90 -> rounding -> 89
2431            (sigmoid_unsigned_midpoint_x1, 1, 89),
2432            // f(10): saturation
2433            (sigmoid_unsigned_midpoint_x10, 10, 99),
2434        }
2435    }
2436 
2437    // --- A5. Zero max_reward: always returns 0 ---
2438    plugin_test! {
2439        model: SigmoidPayout,
2440        input: u128,
2441        output: u128,
2442        context: SigmoidPayoutConfig<u128, FixedU128>,
2443        value: SigmoidPayoutConfig {
2444            max_reward: 0u128,
2445            growth_start: FixedU128::saturating_from_rational(1, 10),
2446            growth_end: FixedU128::saturating_from_rational(9, 10),
2447        },
2448        cases: {
2449            (sigmoid_unsigned_zero_max_x0,   0, 0),
2450            (sigmoid_unsigned_zero_max_x1,   1, 0),
2451            (sigmoid_unsigned_zero_max_x100, 100, 0),
2452        }
2453    }
2454
2455    // B -> SIGNED ASSET
2456
2457    // --- B1. Standard curve: a=0.1, b=0.9, L=100 (same config as A1) ---
2458    plugin_test! {
2459        model: SigmoidPayout,
2460        input: i128,
2461        output: i128,
2462        context: SigmoidPayoutConfig<i128, FixedI128>,
2463        value: SigmoidPayoutConfig {
2464            max_reward: 100i128,
2465            growth_start: FixedI128::saturating_from_rational(1, 10),
2466            growth_end: FixedI128::saturating_from_rational(9, 10),
2467        },
2468        cases: {
2469            (sigmoid_signed_x0_lower_tail, 0, 10),
2470            (sigmoid_signed_x1_upper_tail, 1, 89),
2471            (sigmoid_signed_x5_saturation, 5, 99),
2472            (sigmoid_signed_x100_full_saturation, 100, 100),
2473        }
2474    }
2475 
2476    // --- B2. Negative input (x < 0), further into the lower tail ---
2477    plugin_test! {
2478        model: SigmoidPayout,
2479        input: i128,
2480        output: i128,
2481        context: SigmoidPayoutConfig<i128, FixedI128>,
2482        value: SigmoidPayoutConfig {
2483            max_reward: 100i128,
2484            growth_start: FixedI128::saturating_from_rational(1, 10),
2485            growth_end: FixedI128::saturating_from_rational(9, 10),
2486        },
2487        cases: {
2488            // x=-1: deep in lower tail -> 0
2489            (sigmoid_signed_neg_x1,  -1, 0),
2490            // x=-5: even deeper -> 0
2491            (sigmoid_signed_neg_x5,  -5, 0),
2492        }
2493    }
2494 
2495    // --- B3. Wide curve, signed: a=0.2, b=0.8, L=1000 ---
2496    plugin_test! {
2497        model: SigmoidPayout,
2498        input: i128,
2499        output: i128,
2500        context: SigmoidPayoutConfig<i128, FixedI128>,
2501        value: SigmoidPayoutConfig {
2502            max_reward: 1000i128,
2503            growth_start: FixedI128::saturating_from_rational(2, 10),
2504            growth_end: FixedI128::saturating_from_rational(8, 10),
2505        },
2506        cases: {
2507            (sigmoid_signed_wide_x0, 0, 200),
2508            (sigmoid_signed_wide_x1, 1, 799),
2509            (sigmoid_signed_wide_x10, 10, 999),
2510            // x=-1: far into lower tail, exponent = k*(1+x0) ~= 2.773*1.5 ~= 4.16
2511            //        e^4.16 ~= 64 -> f = 1000/65 ~= 15
2512            (sigmoid_signed_wide_neg_x1, -1, 15),
2513        }
2514    }
2515 
2516    // --- B4. Midpoint at x=0 (a=0.5): negative input -> below L/2 ---
2517    plugin_test! {
2518        model: SigmoidPayout,
2519        input: i128,
2520        output: i128,
2521        context: SigmoidPayoutConfig<i128, FixedI128>,
2522        value: SigmoidPayoutConfig {
2523            max_reward: 100i128,
2524            growth_start: FixedI128::saturating_from_rational(1, 2),
2525            growth_end: FixedI128::saturating_from_rational(9, 10),
2526        },
2527        cases: {
2528            (sigmoid_signed_mid_x0, 0, 50),
2529            (sigmoid_signed_mid_x1, 1, 89),
2530            // x=-1: symmetric to x=1 around x0=0 -> f(-1) = L - f(1) ~= 100-90 = 10
2531            (sigmoid_signed_mid_neg_x1, -1, 10),
2532            // x=-2: f(-2) = L - f(2) ~= 100-98 = 2
2533            (sigmoid_signed_mid_neg_x2, -2, 1),
2534        }
2535    }
2536
2537    // C -> GAURD CONDITIONS
2538
2539    // --- C1. growth_start = 0 (invalid: not strictly in (0,1)) ---
2540    plugin_test! {
2541        model: SigmoidPayout,
2542        input: u128,
2543        output: u128,
2544        context: SigmoidPayoutConfig<u128, FixedU128>,
2545        value: SigmoidPayoutConfig {
2546            max_reward: 100u128,
2547            growth_start: FixedU128::zero(), // INVALID
2548            growth_end: FixedU128::saturating_from_rational(9, 10),
2549        },
2550        cases: {
2551            (sigmoid_guard_unsigned_start_zero, 50, 0),
2552        }
2553    }
2554 
2555    // --- C2. growth_start = 1 (invalid: not strictly in (0,1)) ---
2556    plugin_test! {
2557        model: SigmoidPayout,
2558        input: u128,
2559        output: u128,
2560        context: SigmoidPayoutConfig<u128, FixedU128>,
2561        value: SigmoidPayoutConfig {
2562            max_reward: 100u128,
2563            growth_start: FixedU128::one(),  // INVALID
2564            growth_end: FixedU128::saturating_from_rational(9, 10),
2565        },
2566        cases: {
2567            (sigmoid_guard_unsigned_start_one, 50, 0),
2568        }
2569    }
2570 
2571    // --- C3. growth_end = 0 (invalid) ---
2572    plugin_test! {
2573        model: SigmoidPayout,
2574        input: u128,
2575        output: u128,
2576        context: SigmoidPayoutConfig<u128, FixedU128>,
2577        value: SigmoidPayoutConfig {
2578            max_reward: 100u128,
2579            growth_start: FixedU128::saturating_from_rational(1, 10),
2580            growth_end: FixedU128::zero(), // INVALID
2581        },
2582        cases: {
2583            (sigmoid_guard_unsigned_end_zero, 50, 0),
2584        }
2585    }
2586 
2587    // --- C4. growth_end = 1 (invalid) ---
2588    plugin_test! {
2589        model: SigmoidPayout,
2590        input: u128,
2591        output: u128,
2592        context: SigmoidPayoutConfig<u128, FixedU128>,
2593        value: SigmoidPayoutConfig {
2594            max_reward: 100u128,
2595            growth_start: FixedU128::saturating_from_rational(1, 10),
2596            growth_end: FixedU128::one(), // INVALID
2597        },
2598        cases: {
2599            (sigmoid_guard_unsigned_end_one, 50, 0),
2600        }
2601    }
2602 
2603    // --- C5. growth_start = growth_end -> k = 0, degenerate ---
2604    plugin_test! {
2605        model: SigmoidPayout,
2606        input: u128,
2607        output: u128,
2608        context: SigmoidPayoutConfig<u128, FixedU128>,
2609        value: SigmoidPayoutConfig {
2610            max_reward: 100u128,
2611            growth_start: FixedU128::saturating_from_rational(5, 10), // 0.5
2612            growth_end: FixedU128::saturating_from_rational(5, 10), // 0.5 - same -> k=0
2613        },
2614        cases: {
2615            (sigmoid_guard_unsigned_equal_fracs, 50, 0),
2616        }
2617    }
2618 
2619    // --- C6. Same guards for signed type ---
2620    plugin_test! {
2621        model: SigmoidPayout,
2622        input: i128,
2623        output: i128,
2624        context: SigmoidPayoutConfig<i128, FixedI128>,
2625        value: SigmoidPayoutConfig {
2626            max_reward: 100i128,
2627            growth_start: FixedI128::zero(), // INVALID
2628            growth_end: FixedI128::saturating_from_rational(9, 10),
2629        },
2630        cases: {
2631            (sigmoid_guard_signed_start_zero, 50, 0),
2632        }
2633    }
2634 
2635    plugin_test! {
2636        model: SigmoidPayout,
2637        input: i128,
2638        output: i128,
2639        context: SigmoidPayoutConfig<i128, FixedI128>,
2640        value: SigmoidPayoutConfig {
2641            max_reward: 100i128,
2642            growth_start: FixedI128::saturating_from_rational(1, 10),
2643            growth_end: FixedI128::one(), // INVALID
2644        },
2645        cases: {
2646            (sigmoid_guard_signed_end_one, 50, 0),
2647        }
2648    }
2649 
2650    // --- C7. Inverted curve (growth_start > growth_end) ---
2651    //
2652    // Both parameters are individually valid (in (0,1)), but since
2653    // growth_start > growth_end, the derived slope k wil be negative.
2654    //
2655    // k < 0 inverts the sigmoid:
2656    //   - The curve decays instead of grows
2657    //   - x = 0     -> high value  (~ b * L)
2658    //   - x -> large  -> low value   (~ a * L)
2659    // This is valid behavior for a decreasing reward schedule.
2660    //
2661    // This behavior is not explicitly guarded in the model
2662    // (only k = 0 is guarded), so we document it here as
2663    // a valid "decreasing reward schedule".
2664    plugin_test! {
2665        model: SigmoidPayout,
2666        input: u128,
2667        output: u128,
2668        context: SigmoidPayoutConfig<u128, FixedU128>,
2669        value: SigmoidPayoutConfig {
2670            max_reward:   100u128,
2671            growth_start: FixedU128::saturating_from_rational(9, 10), // 0.9 - inverted
2672            growth_end:   FixedU128::saturating_from_rational(1, 10), // 0.1
2673        },
2674        cases: {
2675            // Decreasing curve -> high at x=0, low at x=1
2676            (sigmoid_inverted_unsigned_x0,  0,  89),
2677            (sigmoid_inverted_unsigned_x1,  1,  10),
2678            // Deep past the curve -> very small
2679            (sigmoid_inverted_unsigned_x10, 10,  0),
2680        }
2681    }
2682
2683    // ----------------------------- INVERSE-PROPORTIONAL-PAYOUT ----------------------------
2684 
2685    plugin_test! {
2686        model: payout::InverseProportionalPayout,
2687        input: u128,
2688        output: u128,
2689        context: payout::InverseProportionalConfig<FixedU128>,
2690        value: payout::InverseProportionalConfig {
2691            k: FixedU128::saturating_from_integer(100),
2692            epsilon: FixedU128::one(),
2693        },
2694        cases: {
2695            (inv_prop_u128_x0,   0,   100),
2696            (inv_prop_u128_x9,   9,   10),
2697            (inv_prop_u128_x99,  99,  1),
2698            (inv_prop_u128_x999, 999, 0),
2699        }
2700    }
2701 
2702    // Signed: negative x -> denom = x + eps may be <= 0 -> return 0 
2703    // With k = 100, eps = 1:
2704    //   x = -1 -> denom = -1 + 1 = 0 -> return 0
2705    //   x = -5 -> denom = -5 + 1 = -4 -> return 0
2706    plugin_test! {
2707        model: payout::InverseProportionalPayout,
2708        input: i128,
2709        output: i128,
2710        context: payout::InverseProportionalConfig<FixedI128>,
2711        value: payout::InverseProportionalConfig {
2712            k: FixedI128::saturating_from_integer(100),
2713            epsilon: FixedI128::one(),
2714        },
2715        cases: {
2716            (inv_prop_i128_positive_x9,   9,   10),
2717            (inv_prop_i128_positive_x99,  99,  1),
2718            // FIX 1: non-positive denom -> 0
2719            (inv_prop_i128_neg_x1,  -1,  0),
2720            (inv_prop_i128_neg_x5,  -5,  0),
2721            // x=0 -> denom = 0 + 1 = 1 -> 100 / 1 = 100
2722            (inv_prop_i128_zero,     0,  100),
2723        }
2724    }
2725
2726    // ---------------------------------- FIXED-RATE-PAYOUT ---------------------------------
2727
2728    plugin_test! {
2729        model: payout::FixedRatePayout,
2730        input: u128,
2731        output: u128,
2732        context: payout::FixedRateConfig<FixedU128>,
2733        value: payout::FixedRateConfig {
2734            rate: FixedU128::saturating_from_rational(5, 100), // 5%
2735        },
2736        cases: {
2737        (fixed_rate_payout_5_percent_of_1000, 1000, 50),
2738        (fixed_rate_payout_5_percent_of_200, 200, 10),
2739        (fixed_rate_payout_5_percent_of_16800, 16800, 840),
2740        (fixed_rate_payout_5_percent_of_0, 0, 0),
2741        }
2742    }
2743
2744    plugin_test! {
2745        model: payout::FixedRatePayout,
2746        input: i128,
2747        output: i128,
2748        context: payout::FixedRateConfig<FixedI128>,
2749        value: payout::FixedRateConfig {
2750            rate: FixedI128::saturating_from_rational(1, 10), // 10%
2751        },
2752        cases: {
2753            (fixed_rate_payout_signed_10_percent_of_370,  -370, -37),
2754            (fixed_rate_payout_signed_10_percent_of_1000, -1000, -100),
2755            (fixed_rate_payout_signed_10_percent_of_max, i128::MIN, -17014118346046923173),
2756        }
2757    }
2758
2759    // --------------------------------- FIXED-ANNUAL-PAYOUT --------------------------------
2760
2761    plugin_test! {
2762        model: payout::FixedAnnualPayout,
2763        input: u128,
2764        output: u128,
2765        context: payout::FixedAnnualConfig<u128, FixedU128>,
2766        value: payout::FixedAnnualConfig {
2767            apr: FixedU128::saturating_from_rational(12, 100), // 12%
2768            time_count: 12u128, // monthly
2769        },
2770        cases: {
2771            // 1000 * (1.12^(1/12) - 1) ~= 9
2772            (fixed_annual_12_percent_monthly_of_1000, 1000, 9),
2773            // 10000 * same ~= 94
2774            (fixed_annual_12_percent_monthly_of_10000, 10000, 94),
2775            (fixed_annual_12_percent_monthly_of_0, 0, 0),
2776        }
2777    }
2778 
2779    plugin_test! {
2780        model: payout::FixedAnnualPayout,
2781        input: u128,
2782        output: u128,
2783        context: payout::FixedAnnualConfig<u128, FixedU128>,
2784        value: payout::FixedAnnualConfig {
2785            apr: FixedU128::zero(), // 0% APR
2786            time_count: 12u128,
2787        },
2788        cases: {
2789            // EPR = (1+0)^(1/12) - 1 = 0 -> reward = 0
2790            (fixed_annual_zero_apr_any_input,  1000, 0),
2791            (fixed_annual_zero_apr_zero_input, 0,    0),
2792        }
2793    }
2794 
2795    // APR = 100%, n = 1 -> EPR = 1.0 -> reward = x
2796    plugin_test! {
2797        model: payout::FixedAnnualPayout,
2798        input: u128,
2799        output: u128,
2800        context: payout::FixedAnnualConfig<u128, FixedU128>,
2801        value: payout::FixedAnnualConfig {
2802            apr: FixedU128::one(), // 100%
2803            time_count: 1u128,
2804        },
2805        cases: {
2806            (fixed_annual_100_percent_n1_of_500,  500,  500),
2807            (fixed_annual_100_percent_n1_of_1000, 1000, 1000),
2808        }
2809    }
2810     
2811    // ---------------------------------- LOGARITHMIC-PAYOUT ---------------------------------
2812
2813    plugin_test! {
2814        model: payout::LogarithmicPayout,
2815        input: u128,
2816        output: u128,
2817        context: payout::LogarithmicConfig<FixedU128>,
2818        value: payout::LogarithmicConfig {
2819            vertical_scale: FixedU128::one(),
2820            horizontal_scale: FixedU128::one(),
2821            horizontal_shift: FixedU128::one(),
2822            vertical_shift: FixedU128::zero(),
2823        },
2824        cases: {
2825            (logarthmic_payout_unsigned_case_1, 1, 0),
2826            (logarthmic_payout_unsigned_case_2, 10, 2),
2827            (logarthmic_payout_unsigned_case_3, 112, 4),
2828            (logarthmic_payout_unsigned_case_4, 7025, 8)
2829        }
2830    }
2831
2832    plugin_test! {
2833        model: payout::LogarithmicPayout,
2834        input: i128,
2835        output: i128,
2836        context: payout::LogarithmicConfig<FixedI128>,
2837        value: payout::LogarithmicConfig {
2838            vertical_scale: FixedI128::one(),
2839            horizontal_scale: FixedI128::one(),
2840            horizontal_shift: FixedI128::one(),
2841            vertical_shift: FixedI128::zero(),
2842        },
2843        cases: {
2844            (logarthmic_payout_signed_case_1, -1, 0),
2845            (logarthmic_payout_signed_case_2, -10, 0),
2846            (logarthmic_payout_signed_case_3, -112, 0),
2847            (logarthmic_payout_signed_case_4, -7025, 0)
2848        }
2849    }
2850
2851    // ------------------------------- PIECEWISE-PAYOUT ------------------------------
2852 
2853    // A -> LINEAR SEGMENT - UNSIGNED TYPES
2854 
2855    // --- A1. Increasing linear [0, 10] -> [0, 100] ---
2856    plugin_test! {
2857        model: PiecewisePayout,
2858        input: u128,
2859        output: u128,
2860        context: PiecewiseConfig<FixedU128>,
2861        value: PiecewiseConfig {
2862            segments: vec![
2863                Segment::Linear {
2864                    start_x: FixedU128::zero(),
2865                    end_x:   FixedU128::saturating_from_integer(10),
2866                    start_y: FixedU128::zero(),
2867                    end_y:   FixedU128::saturating_from_integer(100),
2868                }
2869            ],
2870        },
2871        cases: {
2872            (linear_unsigned_increasing_x0, 0, 0),
2873            (linear_unsigned_increasing_x5, 5, 50),
2874            (linear_unsigned_increasing_x10, 10, 100),
2875        }
2876    }
2877 
2878    // --- A2. Decreasing linear [0, 10] -> [100, 0] ---
2879    plugin_test! {
2880        model: PiecewisePayout,
2881        input: u128,
2882        output: u128,
2883        context: PiecewiseConfig<FixedU128>,
2884        value: PiecewiseConfig {
2885            segments: vec![
2886                Segment::Linear {
2887                    start_x: FixedU128::zero(),
2888                    end_x:   FixedU128::saturating_from_integer(10),
2889                    start_y: FixedU128::saturating_from_integer(100),
2890                    end_y:   FixedU128::zero(),
2891                }
2892            ],
2893        },
2894        cases: {
2895            // x=0: t=0 -> y = 100
2896            (linear_unsigned_decreasing_x0, 0, 100),
2897            // x=5: t=0.5 -> y = 50
2898            (linear_unsigned_decreasing_x5, 5, 50),
2899            // x=10: t=1.0 -> y = 0
2900            (linear_unsigned_decreasing_x10, 10, 0),
2901        }
2902    }
2903 
2904    // --- A3. Decreasing linear [0, 100] -> [1000, 0], larger scale ---
2905    plugin_test! {
2906        model: PiecewisePayout,
2907        input: u128,
2908        output: u128,
2909        context: PiecewiseConfig<FixedU128>,
2910        value: PiecewiseConfig {
2911            segments: vec![
2912                Segment::Linear {
2913                    start_x: FixedU128::zero(),
2914                    end_x:   FixedU128::saturating_from_integer(100),
2915                    start_y: FixedU128::saturating_from_integer(1000),
2916                    end_y:   FixedU128::zero(),
2917                }
2918            ],
2919        },
2920        cases: {
2921            (linear_unsigned_large_decreasing_x0, 0, 1000),
2922            (linear_unsigned_large_decreasing_x25, 25, 750),
2923            (linear_unsigned_large_decreasing_x50, 50, 500),
2924            (linear_unsigned_large_decreasing_x75, 75, 250),
2925            (linear_unsigned_large_decreasing_x100, 100, 0),
2926        }
2927    }
2928 
2929    // --- A4. Flat segment (start_y == end_y), always returns start_y ---
2930    plugin_test! {
2931        model: PiecewisePayout,
2932        input: u128,
2933        output: u128,
2934        context: PiecewiseConfig<FixedU128>,
2935        value: PiecewiseConfig {
2936            segments: vec![
2937                Segment::Linear {
2938                    start_x: FixedU128::zero(),
2939                    end_x:   FixedU128::saturating_from_integer(50),
2940                    start_y: FixedU128::saturating_from_integer(42),
2941                    end_y:   FixedU128::saturating_from_integer(42),
2942                }
2943            ],
2944        },
2945        cases: {
2946            (linear_unsigned_flat_x0, 0, 42),
2947            (linear_unsigned_flat_x25, 25, 42),
2948            (linear_unsigned_flat_x50, 50, 42),
2949        }
2950    }
2951 
2952    // B -> LINEAR SEGMENT - SIGNED TYPES (regression)
2953 
2954    // --- B1. Increasing linear ---
2955    plugin_test! {
2956        model: PiecewisePayout,
2957        input: i128,
2958        output: i128,
2959        context: PiecewiseConfig<FixedI128>,
2960        value: PiecewiseConfig {
2961            segments: vec![
2962                Segment::Linear {
2963                    start_x: FixedI128::zero(),
2964                    end_x:   FixedI128::saturating_from_integer(10),
2965                    start_y: FixedI128::zero(),
2966                    end_y:   FixedI128::saturating_from_integer(100),
2967                }
2968            ],
2969        },
2970        cases: {
2971            (linear_signed_increasing_x0, 0, 0),
2972            (linear_signed_increasing_x5, 5, 50),
2973            (linear_signed_increasing_x10, 10, 100),
2974        }
2975    }
2976 
2977    // --- B2. Decreasing linear ---
2978    plugin_test! {
2979        model: PiecewisePayout,
2980        input: i128,
2981        output: i128,
2982        context: PiecewiseConfig<FixedI128>,
2983        value: PiecewiseConfig {
2984            segments: vec![
2985                Segment::Linear {
2986                    start_x: FixedI128::zero(),
2987                    end_x:   FixedI128::saturating_from_integer(10),
2988                    start_y: FixedI128::saturating_from_integer(100),
2989                    end_y:   FixedI128::zero(),
2990                }
2991            ],
2992        },
2993        cases: {
2994            (linear_signed_decreasing_x0, 0, 100),
2995            (linear_signed_decreasing_x5, 5, 50),
2996            (linear_signed_decreasing_x10, 10, 0),
2997        }
2998    }
2999 
3000    // --- B3. Signed segment with negative y-values, ramp from 0 to -100 ---
3001    plugin_test! {
3002        model: PiecewisePayout,
3003        input: i128,
3004        output: i128,
3005        context: PiecewiseConfig<FixedI128>,
3006        value: PiecewiseConfig {
3007            segments: vec![
3008                Segment::Linear {
3009                    start_x: FixedI128::zero(),
3010                    end_x:   FixedI128::saturating_from_integer(10),
3011                    start_y: FixedI128::zero(),
3012                    end_y:   FixedI128::saturating_from_integer(-100),
3013                }
3014            ],
3015        },
3016        cases: {
3017            (linear_signed_neg_y_x0, 0, 0),
3018            (linear_signed_neg_y_x5, 5, -50),
3019            (linear_signed_neg_y_x10, 10, -100),
3020        }
3021    }
3022 
3023    // C. -> CURVE SEGMENT - UNSIGNED TYPES 
3024
3025    // --- C1. L=100, k=1, x0=5 ---
3026    //
3027    //   f(x) = 100 / (1 + e^(-1*(x - 5)))
3028    //
3029    //   x=0 (left tail):  exp_arg = -(0-5) = +5 -> e^5 ~= 148.41 -> 100/149.41 ~= 0
3030    //   x=2 (left tail):  exp_arg = -(2-5) = +3 -> e^3 ~=  20.09 -> 100/21.09 ~= 4
3031    //   x=4 (left tail):  exp_arg = -(4-5) = +1 -> e^1 ~=   2.72 -> 100/3.72 ~= 26
3032    //   x=5 (midpoint):   exp_arg = 0 -> e^0 = 1 -> 100/2 = 50
3033    //   x=6 (right tail): exp_arg = -(6-5) = -1 -> e^(-1) ~=0.368 -> 100/1.368 ~= 73
3034    //   x=8 (right tail): exp_arg = -(8-5) = -3 -> e^(-3) ~=0.050 -> 100/1.050 ~= 95
3035    //   x=100 (saturation): -> 100
3036    plugin_test! {
3037        model: PiecewisePayout,
3038        input: u128,
3039        output: u128,
3040        context: PiecewiseConfig<FixedU128>,
3041        value: PiecewiseConfig {
3042            segments: vec![
3043                Segment::Curve {
3044                    start_x: FixedU128::zero(),
3045                    end_x:   FixedU128::saturating_from_integer(100),
3046                    params:  CurveParams {
3047                        l:  FixedU128::saturating_from_integer(100),
3048                        k:  FixedU128::one(),
3049                        x0: FixedU128::saturating_from_integer(5),
3050                    },
3051                }
3052            ],
3053        },
3054        cases: {
3055            // Left of x0=5 
3056            (curve_unsigned_left_tail_x0, 0, 0),
3057            (curve_unsigned_left_tail_x2, 2, 4),
3058            (curve_unsigned_left_tail_x4, 4, 26),
3059            // Midpoint
3060            (curve_unsigned_midpoint_x5, 5, 50),
3061            // Right of x0=5
3062            (curve_unsigned_right_tail_x6, 6, 73),
3063            (curve_unsigned_right_tail_x8, 8, 95),
3064            // Saturation
3065            (curve_unsigned_saturation, 100, 100),
3066        }
3067    }
3068 
3069    // --- C2. L=100, k=2, x0=3, unsigned: steeper curve, x0 not at 5 ---
3070    //
3071    //   f(x) = 100 / (1 + e^(-2*(x - 3)))
3072    //
3073    //   x=0: exp_arg = -2*(0-3) = +6 -> e^6 ~= 403.4 -> 100/404.4 ~= 0
3074    //   x=1: exp_arg = -2*(1-3) = +4 -> e^4 ~= 54.6 -> 100/55.6 ~= 1
3075    //   x=2: exp_arg = -2*(2-3) = +2 -> e^2 ~= 7.4 -> 100/8.4 ~= 11
3076    //   x=3: exp_arg = 0 -> 100/2 = 50
3077    //   x=4: exp_arg = -2*(4-3) = -2 -> e^(-2) ~= 0.135-> 100/1.135 ~= 88
3078    //   x=5: exp_arg = -2*(5-3) = -4 -> e^(-4) ~= 0.018-> 100/1.018 ~= 98
3079    plugin_test! {
3080        model: PiecewisePayout,
3081        input: u128,
3082        output: u128,
3083        context: PiecewiseConfig<FixedU128>,
3084        value: PiecewiseConfig {
3085            segments: vec![
3086                Segment::Curve {
3087                    start_x: FixedU128::zero(),
3088                    end_x:   FixedU128::saturating_from_integer(20),
3089                    params:  CurveParams {
3090                        l:  FixedU128::saturating_from_integer(100),
3091                        k:  FixedU128::saturating_from_integer(2),
3092                        x0: FixedU128::saturating_from_integer(3),
3093                    },
3094                }
3095            ],
3096        },
3097        cases: {
3098            (curve_unsigned_steep_x0, 0, 0),
3099            (curve_unsigned_steep_x1, 1, 1),
3100            (curve_unsigned_steep_x2, 2, 11),
3101            (curve_unsigned_steep_x3, 3, 50),
3102            (curve_unsigned_steep_x4, 4, 88),
3103            (curve_unsigned_steep_x5, 5, 98),
3104        }
3105    }
3106 
3107    // --- C3. x0=0 (midpoint at the boundary), x=0 always gives L/2 ---
3108    plugin_test! {
3109        model: PiecewisePayout,
3110        input: u128,
3111        output: u128,
3112        context: PiecewiseConfig<FixedU128>,
3113        value: PiecewiseConfig {
3114            segments: vec![
3115                Segment::Curve {
3116                    start_x: FixedU128::zero(),
3117                    end_x:   FixedU128::saturating_from_integer(20),
3118                    params:  CurveParams {
3119                        l:  FixedU128::saturating_from_integer(100),
3120                        k:  FixedU128::one(),
3121                        x0: FixedU128::zero(), // midpoint at x=0
3122                    },
3123                }
3124            ],
3125        },
3126        cases: {
3127            (curve_unsigned_x0_at_midpoint, 0, 50),
3128            (curve_unsigned_x0_right_x1, 1, 73),
3129            (curve_unsigned_x0_right_x5, 5, 99),
3130        }
3131    }
3132 
3133    // D. CURVE SEGMENT - SIGNED TYPES (regression)
3134 
3135    // --- D1. L=100, k=1, x0=5 ---
3136    plugin_test! {
3137        model: PiecewisePayout,
3138        input: i128,
3139        output: i128,
3140        context: PiecewiseConfig<FixedI128>,
3141        value: PiecewiseConfig {
3142            segments: vec![
3143                Segment::Curve {
3144                    start_x: FixedI128::zero(),
3145                    end_x:   FixedI128::saturating_from_integer(100),
3146                    params:  CurveParams {
3147                        l:  FixedI128::saturating_from_integer(100),
3148                        k:  FixedI128::one(),
3149                        x0: FixedI128::saturating_from_integer(5),
3150                    },
3151                }
3152            ],
3153        },
3154        cases: {
3155            (curve_signed_at_midpoint, 5, 50),
3156            (curve_signed_saturation, 100, 100),
3157            (curve_signed_at_zero, 0, 0),
3158            (curve_signed_left_x2, 2, 4),
3159            (curve_signed_left_x4, 4, 26),
3160            (curve_signed_right_x6, 6, 73),
3161            (curve_signed_right_x8, 8, 95),
3162        }
3163    }
3164 
3165    // --- D2. Signed input with negative x (x < 0), x0=5 ---
3166    //
3167    //   x=-2: diff = -2 - 5 = -7 -> exp_arg = +7 -> e^7 ~= 1096 -> 100/1097 ~= 0
3168    //   x=-5: diff = -10 -> exp_arg = +10 -> e^10 ~= 22026 -> ~= 0
3169    plugin_test! {
3170        model: PiecewisePayout,
3171        input: i128,
3172        output: i128,
3173        context: PiecewiseConfig<FixedI128>,
3174        value: PiecewiseConfig {
3175            segments: vec![
3176                Segment::Curve {
3177                    start_x: FixedI128::saturating_from_integer(-10),
3178                    end_x:   FixedI128::saturating_from_integer(100),
3179                    params:  CurveParams {
3180                        l:  FixedI128::saturating_from_integer(100),
3181                        k:  FixedI128::one(),
3182                        x0: FixedI128::saturating_from_integer(5),
3183                    },
3184                }
3185            ],
3186        },
3187        cases: {
3188            (curve_signed_neg_x2, -2, 0),
3189            (curve_signed_neg_x5, -5, 0),
3190            (curve_signed_midpoint, 5, 50),
3191        }
3192    }
3193 
3194    // E -> MULTI-SEGMENT COMPOSITIONS
3195 
3196    // --- E1. Linear increase -> decreasing linear (triangle wave) ---
3197    //
3198    //   Seg 1: [0, 5]  -> [0, 100] (ramp up)
3199    //   Seg 2: [5, 10] -> [100, 0] (ramp down)
3200    //
3201    //   x=0:  Seg 1, t=0 -> 0
3202    //   x=2:  Seg 1, t=0.4 -> 40
3203    //   x=5:  Seg 1 matches (x <= end_x=5), t=1.0 -> 100 (first-match wins)
3204    //   x=7:  Seg 2, t=0.4 -> 60
3205    //   x=10: Seg 2, t=1.0 -> 0
3206    plugin_test! {
3207        model: PiecewisePayout,
3208        input: u128,
3209        output: u128,
3210        context: PiecewiseConfig<FixedU128>,
3211        value: PiecewiseConfig {
3212            segments: vec![
3213                Segment::Linear {
3214                    start_x: FixedU128::zero(),
3215                    end_x:   FixedU128::saturating_from_integer(5),
3216                    start_y: FixedU128::zero(),
3217                    end_y:   FixedU128::saturating_from_integer(100),
3218                },
3219                Segment::Linear {
3220                    start_x: FixedU128::saturating_from_integer(5),
3221                    end_x:   FixedU128::saturating_from_integer(10),
3222                    start_y: FixedU128::saturating_from_integer(100),
3223                    end_y:   FixedU128::zero(),
3224                },
3225            ],
3226        },
3227        cases: {
3228            (multi_triangle_x0, 0, 0),
3229            (multi_triangle_x2, 2, 40),
3230            (multi_triangle_x5, 5, 100), // first segment matches at x=5
3231            (multi_triangle_x7, 7, 60),
3232            (multi_triangle_x10, 10, 0),
3233        }
3234    }
3235 
3236    // --- E2. Linear ramp -> Curve saturation (bootstrapping schedule) ---
3237    //
3238    //   Seg 1: Linear [0, 10] -> [0, 50] (ramp up)
3239    //   Seg 2: Curve  [10, 100], L=100, k=1, x0=10
3240    //
3241    //   x=5:   Seg 1, t=0.5 -> 25
3242    //   x=10:  Seg 1 (first match), t=1.0 -> 50
3243    //   x=15:  Seg 2, diff=5, exp_arg=-5 -> e^(-5)~=0.0067 -> 100/1.0067~=99
3244    //   x=100: Seg 2, saturation -> 100
3245    plugin_test! {
3246        model: PiecewisePayout,
3247        input: u128,
3248        output: u128,
3249        context: PiecewiseConfig<FixedU128>,
3250        value: PiecewiseConfig {
3251            segments: vec![
3252                Segment::Linear {
3253                    start_x: FixedU128::zero(),
3254                    end_x:   FixedU128::saturating_from_integer(10),
3255                    start_y: FixedU128::zero(),
3256                    end_y:   FixedU128::saturating_from_integer(50),
3257                },
3258                Segment::Curve {
3259                    start_x: FixedU128::saturating_from_integer(10),
3260                    end_x:   FixedU128::saturating_from_integer(100),
3261                    params:  CurveParams {
3262                        l:  FixedU128::saturating_from_integer(100),
3263                        k:  FixedU128::one(),
3264                        x0: FixedU128::saturating_from_integer(10),
3265                    },
3266                },
3267            ],
3268        },
3269        cases: {
3270            (multi_ramp_curve_x5, 5, 25),
3271            (multi_ramp_curve_x10, 10, 50), // linear segment matches first
3272            (multi_ramp_curve_x15, 15, 99),
3273            (multi_ramp_curve_x100, 100, 100),
3274        }
3275    }
3276 
3277    // --- E3. Gap between segments: x in the gap -> 0 ---
3278    //
3279    //   Seg 1: [0, 5]
3280    //   Seg 2: [20, 30]
3281    //   x=10: no segment matches -> 0
3282    plugin_test! {
3283        model: PiecewisePayout,
3284        input: u128,
3285        output: u128,
3286        context: PiecewiseConfig<FixedU128>,
3287        value: PiecewiseConfig {
3288            segments: vec![
3289                Segment::Linear {
3290                    start_x: FixedU128::zero(),
3291                    end_x:   FixedU128::saturating_from_integer(5),
3292                    start_y: FixedU128::zero(),
3293                    end_y:   FixedU128::saturating_from_integer(50),
3294                },
3295                Segment::Linear {
3296                    start_x: FixedU128::saturating_from_integer(20),
3297                    end_x:   FixedU128::saturating_from_integer(30),
3298                    start_y: FixedU128::saturating_from_integer(80),
3299                    end_y:   FixedU128::saturating_from_integer(100),
3300                },
3301            ],
3302        },
3303        cases: {
3304            (multi_gap_in_range_seg1, 5, 50),
3305            (multi_gap_between, 10, 0),
3306            (multi_gap_in_range_seg2, 20, 80),
3307        }
3308    }
3309 
3310    // F -> EDGE / GUARD CASES
3311 
3312    // --- F1. Empty segment list -> 0 for any input ---
3313    plugin_test! {
3314        model: PiecewisePayout,
3315        input: u128,
3316        output: u128,
3317        context: PiecewiseConfig<FixedU128>,
3318        value: PiecewiseConfig {
3319            segments: vec![],
3320        },
3321        cases: {
3322            (empty_segments_x0, 0, 0),
3323            (empty_segments_x42, 42, 0),
3324        }
3325    }
3326 
3327    // --- F2. Out-of-range input -> 0 ---
3328    plugin_test! {
3329        model: PiecewisePayout,
3330        input: u128,
3331        output: u128,
3332        context: PiecewiseConfig<FixedU128>,
3333        value: PiecewiseConfig {
3334            segments: vec![
3335                Segment::Linear {
3336                    start_x: FixedU128::zero(),
3337                    end_x:   FixedU128::saturating_from_integer(10),
3338                    start_y: FixedU128::zero(),
3339                    end_y:   FixedU128::saturating_from_integer(100),
3340                }
3341            ],
3342        },
3343        cases: {
3344            (out_of_range_above, 50, 0),
3345        }
3346    }
3347 
3348    // --- F3. Degenerate linear segment (start_x == end_x) -> always start_y ---
3349    //
3350    //   width = 0, so we return start_y unconditionally
3351    plugin_test! {
3352        model: PiecewisePayout,
3353        input: u128,
3354        output: u128,
3355        context: PiecewiseConfig<FixedU128>,
3356        value: PiecewiseConfig {
3357            segments: vec![
3358                Segment::Linear {
3359                    start_x: FixedU128::saturating_from_integer(5),
3360                    end_x:   FixedU128::saturating_from_integer(5), // same as start
3361                    start_y: FixedU128::saturating_from_integer(77),
3362                    end_y:   FixedU128::saturating_from_integer(99),
3363                }
3364            ],
3365        },
3366        cases: {
3367            (degenerate_linear_at_point, 5, 77),
3368        }
3369    }
3370 
3371    // --- F4. Curve with k=0: e^0 = 1 always -> L/2 everywhere ---
3372    plugin_test! {
3373        model: PiecewisePayout,
3374        input: u128,
3375        output: u128,
3376        context: PiecewiseConfig<FixedU128>,
3377        value: PiecewiseConfig {
3378            segments: vec![
3379                Segment::Curve {
3380                    start_x: FixedU128::zero(),
3381                    end_x:   FixedU128::saturating_from_integer(100),
3382                    params:  CurveParams {
3383                        l:  FixedU128::saturating_from_integer(100),
3384                        k:  FixedU128::zero(),  // k=0 -> exp_arg=0 -> e^0=1 -> L/2
3385                        x0: FixedU128::saturating_from_integer(5),
3386                    },
3387                }
3388            ],
3389        },
3390        cases: {
3391            (curve_k_zero_x0,  0,  50),
3392            (curve_k_zero_x50, 50, 50),
3393        }
3394    }
3395
3396    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3397    // ````````````````````````````````` PAYEE MODELS ````````````````````````````````
3398    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3399        
3400    // ---------------------------------- SHARES-PAY ---------------------------------
3401
3402    plugin_test! {
3403        model: payee::SharesPay,
3404        input: (u128, Vec<(AccountId32, u128)>),
3405        output: Vec<(AccountId32, u128)>,
3406        cases: {
3407            // 100 payout, shares: [50, 50] => each gets 50
3408            (shares_pay_equal_split, (100, vec![(ALICE, 50), (BOB, 50)]), vec![(ALICE, 50), (BOB, 50)]),
3409            // 90 payout, shares: [30, 60] => 30/90=1/3*90=30, 60/90=2/3*90=60
3410            (shares_pay_proportional_split, (90, vec![(ALICE, 30), (BOB, 60)]), vec![(ALICE, 30), (BOB, 60)]),
3411            // 0 payout, shares: [10, 20] => all get 0
3412            (shares_pay_zero_payout, (0, vec![(ALICE, 10), (BOB, 20)]), vec![(ALICE, 0), (BOB, 0)]),
3413            // payout with zero shares
3414            (shares_pay_all_zero_shares, (100, vec![(ALICE, 0), (BOB, 0)]), vec![]),
3415            // payout with empty payees
3416            (shares_pay_no_payees, (100, vec![]), vec![]),
3417        }
3418    }
3419
3420    // ---------------------------------- EQUAL-PAY ----------------------------------
3421
3422    plugin_test! {
3423        model: payee::EqualPay,
3424        input: (u128, Vec<(AccountId32, u128)>),
3425        output: Vec<(AccountId32, u128)>,
3426        cases: {
3427        // 100 payout, 2 payees: each gets 50
3428        (equal_pay_two_payees_even_split, (100, vec![(ALICE, 10), (BOB, 20)]), vec![(ALICE, 50), (BOB, 50)]),
3429        // 90 payout, 3 payees: each gets 30
3430        (equal_pay_three_payees_even_split, (90, vec![(ALICE, 1u128), (BOB, 1u128), (MIKE, 1u128)]), vec![(ALICE, 30), (BOB, 30), (MIKE, 30)]),
3431        (equal_pay_three_payees_with_remainder, (100, vec![(ALICE, 1u128), (BOB, 1u128), (MIKE, 1u128)]), vec![(ALICE, 33u128), (BOB, 33u128), (MIKE, 33u128)]),
3432        // 0 payout, 2 payees: each gets 0
3433        (equal_pay_zero_payout, (0, vec![(ALICE, 10), (BOB, 20)]), vec![(ALICE, 0), (BOB, 0)]),
3434        // payout with empty payees
3435        (equal_pay_no_payees, (100, vec![]), vec![]),
3436        }
3437    }
3438}