frame_plugins/
balances.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// ````````````````````````````` LAZY BALANCE PLUGINS ````````````````````````````
14// ===============================================================================
15
16//! Lazy balance plugin families built on top of
17//! [`LazyBalanceRoot`](frame_suite::assets::LazyBalanceRoot).
18//!
19//! This module defines reusable `plugin families` that implement different
20//! lazy balance models using the [`LazyBalance`](frame_suite::assets::LazyBalance)
21//! interface.
22//!
23//! Each family provides:
24//! - execution logic (deposit, withdraw, mint, reap, drain)
25//! - validation (`Can*` plugins)
26//! - read-only queries
27//! - [`virtual balance`](frame_suite::virtuals)
28//! structure accessors.
29//!
30//! Use a specific family (e.g. [`ShareBalanceFamily`]) together with its
31//! context to integrate a concrete lazy balance model.
32
33// ===============================================================================
34// ```````````````````````````````` SHARE-BALANCE ````````````````````````````````
35// ===============================================================================
36pub use share_balance::*;
37
38/// Share-based lazy balance model implementation.
39///
40/// Provides [`ShareBalanceFamily`] and [`ShareBalanceContext`] for
41/// a proportional ownership (shares) based
42/// [`LazyBalance`](frame_suite::assets::LazyBalance) model.
43///
44/// Use:
45/// - [`ShareBalanceFamily`]: plugin family (execution + validation)
46/// - [`ShareBalanceContext`]: context binding for the implementation
47///
48/// This model tracks ownership via shares and resolves value lazily
49/// at withdrawal time.
50mod share_balance {
51
52    // ===============================================================================
53    // ``````````````````````````````````` IMPORTS ```````````````````````````````````
54    // ===============================================================================
55
56    // --- Core (Rust std replacement) ---
57    use core::marker::PhantomData;
58
59    // --- Scale-codec crates ---
60    use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
61    use scale_info::TypeInfo;
62
63    // --- FRAME Suite ---
64    use frame_suite::{
65        assets::*, define_family, empty_virtual_extension, misc::Directive, mutation::MutHandle, plugin_model,
66        virtuals::*,
67    };
68
69    // --- FRAME Support ---
70    use frame_support::traits::tokens::{Fortitude, Precision};
71
72    // --- Substrate primitives ---
73    use sp_core::ConstU32;
74    use sp_runtime::{
75        traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, One, Zero},
76        Cow, DispatchError, FixedPointNumber, Saturating, Vec,
77    };
78
79    // ===============================================================================
80    // ````````````````````````` SHARE-BALANCE PLUGIN FAMILY `````````````````````````
81    // ===============================================================================
82
83    define_family! {
84        // Root trait defining the LazyBalance execution surface
85        root: LazyBalanceRoot,
86
87        /// Plugin family implementing a **share-based lazy balance model**.
88        ///
89        /// ## Model
90        ///
91        /// Value is tracked via **shares (proportional ownership)**:
92        ///
93        /// ```text
94        /// deposit  -> mint shares
95        /// mint/reap -> mutate balance state
96        /// withdraw -> redeem shares at current share-price
97        /// ```
98        ///
99        /// - Receipts encode **shares**, not fixed value
100        /// - Balance mutations affect only the given **balance state**
101        /// - Final value is resolved **lazily at withdrawal**
102        ///
103        /// ## Complexity
104        ///
105        /// All operations are **O(1)**:
106        /// - no iteration or global recomputation
107        ///
108        /// ## Constraints
109        ///
110        /// Operations are intentionally **unbounded**:
111        /// - `deposit`, `mint`, `reap` has no intrinsic limits
112        ///
113        /// Minimal invariants:
114        ///
115        /// - No `deposit` after a full drain (complete reap)
116        ///   - requires a `mint` to reinitialize the balance
117        /// - No `mint` or `reap` before any deposit exists
118        ///
119        /// ## Edge Conditions
120        ///
121        /// Arbitrary or unstructured use of `mint`/`reap`
122        /// (e.g. without a consistent economic model) can lead to
123        /// **skewed redemption outcomes** at withdrawal which reflects
124        /// **misuse**, not a violation of system invariants.
125        ///
126        /// The design permits unrestricted operations, but assumes
127        /// coherent, policy-driven execution in production
128        ///
129        /// ## Dust Handling
130        ///
131        /// Any residual balance ("dust") caused by rounding or share
132        /// precision is not redistributed.
133        ///
134        /// The **last withdrawer of the final receipt** receives the
135        /// entire remaining dust in the balance.
136        ///
137        /// ## Lifetime
138        ///
139        /// The lifetime `'a` ties plugin execution to the caller's borrow scope,
140        /// allowing a mutable reference to the `balance` to be passed into the plugin
141        /// and safely used across operation boundaries.
142        family: pub ShareBalanceFamily,
143
144        // Lifetimes for mutable borrow of (balance) carried by the family marker
145        // (execution-time borrowing)
146        // Propagated through LazyBalance::Input/Output into all models
147        borrow: ['a],
148
149        // Input / Output carriers (discriminanted-dispatched across plugins)
150        input: In,
151        output: Out,
152
153        // Context type providing layout, environment, and error mapping
154        context: ShareBalanceContext<T>,
155
156        // Generics applied on the impl (context specialization)
157        // T binds the LazyBalance implementation into the context
158        marker: [T],
159
160        bounds: [
161            // Core contract: provides associated types + execution interface
162            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
163
164            // Error translation layer across all plugin executions
165            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
166
167            // Output carrier must support all operation result shapes
168            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
169
170            // Input carrier must support all operation parameter shapes
171            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
172
173            // Required for fixed-point -> asset conversion (withdraw path)
174            T::Asset: From<<T::Rational as FixedPointNumber>::Inner>,
175        ],
176
177        child: [
178            // --- State mutations (modify balance state) ---
179            Deposit => ModelDeposit,     // issues shares for value
180            Mint => ModelMint,           // increases price per share (bias +)
181            Reap => ModelReap,           // decreases price per share (bias -)
182            Withdraw => ModelWithdraw,   // resolves receipt -> burns shares
183            Drain => ModelDrain,         // full depletion (Reap(total_value))
184
185            // --- Validation (pure pre-checks, no mutation) ---
186            CanDeposit => ModelCanDeposit,
187            CanMint => ModelCanMint,
188            CanReap => ModelCanReap,
189            CanWithdraw => ModelCanWithdraw,
190
191            // --- Read-only queries (state inspection) ---
192            TotalValue => ModelTotalValue,                   // total balance value
193            ReceiptActiveValue => ModelReceiptActiveValue,   // simulated withdraw value
194            HasDeposits => ModelHasDeposits,                 // issued > 0
195            ReceiptDepositValue => ModelReceiptDepositValue, // original deposit value
196
197            // --- Limits (policy layer: unbounded/permissive in this model) ---
198            DepositLimits => ModelDepositLimits,
199            MintLimits => ModelMintLimits,
200            ReapLimits => ModelReapLimits,
201        ]
202    }
203
204    // ===============================================================================
205    // ```````````````````````` SHARE-BALANCE INTERNAL HELPERS ```````````````````````
206    // ===============================================================================
207
208    /// Advances the checkpoint on balance updates (mint/reap).
209    ///
210    /// The checkpoint is a monotonically increasing counter that represents
211    /// logical time. It is incremented whenever the balance changes, i.e.,
212    /// when the share price (bias) is updated which helps track when deposits
213    /// were made relative to balance state changes.
214    ///
215    /// This is mainly useful for handling drain scenarios. If a full reap
216    /// (drain) occurs after a deposit, earlier receipts may no longer
217    /// be meaningful. A drain resets the share price (bias) to zero, so
218    /// during withdrawal the receipt's original pricing context becomes
219    /// outdated, even though the shares still exist.
220    ///
221    /// The checkpoint and drain point (stored in both balance and receipt)
222    /// allow withdrawals to efficiently detect and handle such cases.
223    ///
224    /// This keeps withdrawal logic simple and `O(1)` while maintaining
225    /// correctness across balance resets.
226    fn balance_checkpoint<T: LazyBalance>(
227        balance: &mut T::Balance,
228    ) -> Result<(), ShareBalanceError> {
229        let checkpoint = balance::checkpoint::<T>(balance)
230            .ok_or(ShareBalanceError::BalanceNotInitiatedViaDeposit)?;
231
232        let bias =
233            balance::bias::<T>(balance).ok_or(ShareBalanceError::BalanceNotInitiatedViaDeposit)?;
234
235        if bias.is_zero() {
236            balance::set_drainpoint::<T>(balance, checkpoint.saturating_add(One::one()))?;
237        }
238
239        balance::set_checkpoint::<T>(balance, checkpoint.saturating_add(One::one()))?;
240
241        Ok(())
242    }
243
244    // ===============================================================================
245    // ```````````````````````````````` PLUGIN MODELS ````````````````````````````````
246    // ===============================================================================
247
248    /// Plugin execution context for [`ShareBalanceFamily`].
249    ///
250    /// The generic `T` is expected to be a concrete implementation of
251    /// [`LazyBalance`], defining the core types this context operates on.
252    ///
253    /// Implements [`LazyBalanceContext`], providing bounds, extension schemas,
254    /// and error typing required by the balance model.
255    pub struct ShareBalanceContext<T>(pub PhantomData<T>);
256
257    plugin_model!(
258
259        /// [`Deposit`] plugin family's child model over the
260        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
261        ///
262        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::Deposit`].
263        ///
264        /// ## Overview
265        ///
266        /// Accepts an asset deposit and issues a [`LazyBalance::Receipt`]
267        /// representing a proportional claim over the balance along with
268        /// the total deposited amount.
269        ///
270        /// Effects:
271        /// - increases `effective`
272        /// - increases `issued` (shares)
273        /// - returns a receipt encoding the depositor's stake
274        ///
275        /// ## Share Derivation
276        ///
277        /// Shares are computed relative to current state:
278        ///
279        /// - `issued == 0` -> shares = asset (bootstrap)
280        /// - otherwise     -> shares = asset / bias (share-price)
281        ///
282        /// where `bias = effective / issued`.
283        ///
284        /// Rational results are floored when converting to integer shares,
285        /// preventing implicit value creation and allowing fractional
286        /// dust to accumulate.
287        ///
288        /// ## Constraints
289        ///
290        /// - zero-value deposits are rejected
291        /// - fresh balances are initialized on first deposit
292        /// - deposits are disallowed if `bias == 0 && issued != 0`
293        ///   (fully drained state; requires mint to recover the bias)
294        ///
295        /// ## Receipt
296        ///
297        /// The issued [`LazyBalance::Receipt`] contains:
298        /// - `principal` : deposited value
299        /// - `shares`    : issued shares
300        /// - `bias`      : balance bias
301        /// - `checkpoint`: balance checkpoint
302        ///
303        /// Receipts encode relative ownership, not fixed value.
304        name: pub ModelDeposit,
305        input: In,
306        output: Out,
307        others: ['a, T],
308        context: ShareBalanceContext<T>,
309        bounds : [
310            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
311            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
312            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
313            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
314            T::Asset: From<<T::Rational as FixedPointNumber>::Inner>,
315        ],
316        compute: |input, _context| {
317            let Ok((mut balance, variant, id, asset, subject)) = TryIntoTag::<_, Deposit>::try_into_tag(input) else {
318                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
319            };
320
321            let balance = &mut balance;
322
323            // Deposit amount must be non-zero
324            if asset.is_zero() {
325                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::ZeroDepositNotAllowed))
326            }
327
328            // Initialize balance if this is the first interaction
329            if let Err(e) = balance::is_fresh_balance::<T>(balance) {
330                return <Out as FromTag::<_, Deposit>>::from_tag(Err(e))
331            }
332
333            let Some(bias) = balance::bias::<T>(balance) else {
334                // unreachable unless balance virtual dyn field is corrupted
335                debug_assert!(
336                    false,
337                    "corrupted virtual dyn field balance::bias during \
338                    ShareBalanceFamily::Deposit for id {:?}, variant {:?} \
339                    amount {:?} subject {:?}",
340                    variant, id, asset, subject
341                );
342                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField));
343            };
344
345            let Some(effective) = balance::effective::<T>(balance) else {
346                // unreachable unless balance virtual dyn field is corrupted
347                debug_assert!(
348                    false,
349                    "corrupted virtual dyn field balance::effective during \
350                    ShareBalanceFamily::Deposit for id {:?}, variant {:?} \
351                    amount {:?} subject {:?}",
352                    variant, id, asset, subject
353                );
354                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField));
355            };
356
357            let Some(issued) = balance::issued::<T>(balance) else {
358                // unreachable unless balance virtual dyn field is corrupted
359                debug_assert!(
360                    false,
361                    "corrupted virtual dyn field balance::issued during \
362                    ShareBalanceFamily::Deposit for id {:?}, variant {:?} \
363                    amount {:?} subject {:?}",
364                    variant, id, asset, subject
365                );
366                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField));
367            };
368
369            // Disallow deposits on drained balance (bias == 0)
370            // Requires mint to re-establish share pricing
371            if bias.is_zero() {
372                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::BalanceDrainedCannotDeposit))
373            }
374
375            // Derive shares:
376            // - bootstrap: 1:1 mapping
377            // - otherwise: asset / bias (price per share)
378            let shares = match issued.is_zero() {
379                true => *asset,
380                false => {
381                    let Some(div) = T::Rational::saturating_from_integer(*asset).checked_div(&bias) else {
382                        // invalid bias state (overflow / underflow domain)
383                        return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
384                    };
385                    let Some(actual) = div.into_inner().checked_div(&T::Rational::DIV) else {
386                        // unreachable unless DIV is zero
387                        debug_assert!(
388                            false,
389                            "divide by zero during fixed-point scaling during \
390                            ShareBalanceFamily::Deposit for id {:?}, variant {:?} \
391                            amount {:?} subject {:?}",
392                            variant, id, asset, subject
393                        );
394                        return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::FixedPointScalingFailed))
395                    };
396                    // implicit flooring -> prevents value creation, accumulates fractional dust
397                    actual.into()
398                }
399            };
400
401            // Reject deposits that resolve to zero shares (too small may be under one due to flooring)
402            if shares.is_zero() {
403                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::LessThanOneShareDerived))
404            }
405
406            // Update effective balance (real value)
407            let Some(new_effective) = effective.checked_add(&asset) else {
408                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::AssetOverflow))
409            };
410
411            // Update total issued shares
412            let Some(new_issued) = issued.checked_add(&shares) else {
413                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::SharesOverflow))
414            };
415
416            if let Err(e) = balance::set_effective::<T>(balance, new_effective) {
417                return <Out as FromTag::<_, Deposit>>::from_tag(Err(e));
418            };
419
420            if let Err(e) = balance::set_issued::<T>(balance, new_issued) {
421                return <Out as FromTag::<_, Deposit>>::from_tag(Err(e));
422            };
423
424            // Create new receipt (initially empty virtual struct)
425            let mut receipt = T::Receipt::default();
426
427            // Capture checkpoint -> anchors future withdrawal derivation
428            let Some(checkpoint) = balance::checkpoint::<T>(balance) else {
429                // unreachable unless balance virtual dyn field is corrupted
430                debug_assert!(
431                    false,
432                    "corrupted virtual dyn field balance::checkpoint during \
433                    ShareBalanceFamily::Deposit for id {:?}, variant {:?} \
434                    amount {:?} subject {:?}",
435                    variant, id, asset, subject
436                );
437                return <Out as FromTag::<_, Deposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField));
438            };
439
440            // Store original deposit value
441            if let Err(e) = receipt::set_principal::<T>(&mut receipt, *asset) {
442                return <Out as FromTag::<_, Deposit>>::from_tag(Err(e));
443            };
444
445            // Store issued shares at deposit time
446            if let Err(e) = receipt::set_shares::<T>(&mut receipt, shares) {
447                return <Out as FromTag::<_, Deposit>>::from_tag(Err(e));
448            };
449
450            // Store pricing context
451            receipt::set_bias::<T>(&mut receipt, bias);
452
453            // Store time anchor
454            receipt::set_checkpoint::<T>(&mut receipt, checkpoint);
455
456            // Return (deposited asset, receipt)
457            <Out as FromTag::<_, Deposit>>::from_tag(Ok((asset, Cow::Owned(receipt))))
458        }
459    );
460
461    plugin_model!(
462        /// [`CanDeposit`] plugin family's child model over the
463        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
464        ///
465        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::CanDeposit`].
466        ///
467        /// ## Overview
468        ///
469        /// Performs pre-checks to determine whether a deposit is allowed,
470        /// without mutating balance state.
471        ///
472        /// ## Validation Rules
473        ///
474        /// - deposit amount must be non-zero
475        /// - addition to balance's `effective` must not overflow
476        /// - deposits are disallowed if `bias == 0 && issued != 0`
477        ///   (balance is fully drained and requires minting)
478        ///
479        /// ## Fresh Balance
480        ///
481        /// If the balance is uninitialized (default `effective` missing),
482        /// the deposit is considered valid and initialization is deferred
483        /// to the deposit operation itself.
484        ///
485        /// ## Semantics
486        ///
487        /// This check mirrors [`ModelDeposit`] model constraints without applying
488        /// state transitions, ensuring that execution will succeed if
489        /// validation passes.
490        name: pub ModelCanDeposit,
491        input: In,
492        output: Out,
493        others: ['a, T, ],
494        context: ShareBalanceContext<T>,
495        bounds : [
496            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
497            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
498            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
499            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
500        ],
501        compute: |input, _context| {
502
503            let Ok((balance, variant, id, asset, subject)) = TryIntoTag::<_, CanDeposit>::try_into_tag(input) else {
504                return <Out as FromTag::<_, CanDeposit>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
505            };
506
507            // Fresh balance -> allow deposit (initialization deferred to execution)
508            let Some(effective) = balance::effective::<T>(&balance) else {
509                return <Out as FromTag::<_, CanDeposit>>::from_tag(Ok(()))
510            };
511
512            let Some(issued) = balance::issued::<T>(&balance) else {
513                // unreachable unless balance virtual dyn field is corrupted
514                debug_assert!(
515                    false,
516                    "corrupted virtual dyn field balance::issued during \
517                    ShareBalanceFamily::CanDeposit for id {:?}, variant {:?} \
518                    amount {:?} subject {:?}",
519                    variant, id, asset, subject
520                );
521                return <Out as FromTag::<_, CanDeposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField))
522            };
523
524            let Some(bias) = balance::bias::<T>(&balance) else {
525                // unreachable unless balance virtual dyn field is corrupted
526                debug_assert!(
527                    false,
528                    "corrupted virtual dyn field balance::bias during \
529                    ShareBalanceFamily::CanDeposit for id {:?}, variant {:?} \
530                    amount {:?} subject {:?}",
531                    variant, id, asset, subject
532                );
533                return <Out as FromTag::<_, CanDeposit>>::from_tag(Err(ShareBalanceError::CorruptedVirtualField))
534            };
535
536            // Disallow deposits on bankrupt i.e., drained balance (bias == 0 && issued != 0)
537            if bias.is_zero() && !issued.is_zero() {
538                return <Out as FromTag::<_, CanDeposit>>::from_tag(
539                    Err(ShareBalanceError::BalanceDrainedCannotDeposit)
540                )
541            }
542
543            // Ensure deposit does not overflow effective balance
544            if effective.checked_add(&asset).is_none() {
545                return <Out as FromTag::<_, CanDeposit>>::from_tag(
546                    Err(ShareBalanceError::AssetOverflow)
547                )
548            }
549
550            <Out as FromTag::<_, CanDeposit>>::from_tag(Ok(()))
551        }
552    );
553
554    plugin_model!(
555        /// [`Mint`] plugin family's child model over the
556        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
557        ///
558        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::Mint`].
559        ///
560        /// ## Overview
561        ///
562        /// Introduces new value into the balance without issuing new shares.
563        ///
564        /// This increases balance's `effective` while keeping `issued` untouched,
565        /// thereby increasing `bias` (value per share).
566        ///
567        /// ## Effect
568        ///
569        /// - `effective += asset`
570        /// - `issued` remains unchanged
571        /// - `bias = effective / issued` is recomputed
572        ///
573        /// This distributes value proportionally across all existing shares
574        /// implicitly and lazily acquired during withdrawal.
575        ///
576        /// ## Constraints
577        ///
578        /// - zero-value mint is a no-op
579        /// - balance must not be fresh (must be initialized)
580        /// - balance must have existing deposits (`issued > 0`)
581        /// - addition to `effective` must not overflow
582        ///
583        /// ## Semantics
584        ///
585        /// Minting represents an external value injection:
586        /// - no new ownership is created
587        /// - all existing receipts gain value proportionally lazily
588        ///
589        /// This is the only way to revive a fully drained balance
590        /// (where `bias == 0 && issued != 0`).
591        name: pub ModelMint,
592        input: In,
593        output: Out,
594        others: ['a, T,],
595        context: ShareBalanceContext<T>,
596        bounds : [
597            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
598            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
599            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
600            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
601        ],
602        compute: |input, _context| {
603
604            let Ok((mut balance, _variant, _id, asset, _subject)) = TryIntoTag::<_, Mint>::try_into_tag(input) else {
605                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
606            };
607
608            // Zero mint -> no-op
609            if asset.is_zero() {
610                return <Out as FromTag::<_, Mint>>::from_tag(Ok(Cow::Owned(Zero::zero())));
611            }
612
613            let balance = &mut balance;
614
615            // Uninitialized balance
616            let Some(effective) = balance::effective::<T>(balance) else {
617                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
618            };
619
620            // Uninitialized balance
621            let Some(issued) = balance::issued::<T>(balance) else {
622                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
623            };
624
625            // Mint requires existing shares (cannot bootstrap)
626            if issued.is_zero() {
627                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::RequiresExistingDeposits))
628            };
629
630            // Increase effective balance
631            let Some(new_effective) = effective.checked_add(&asset) else {
632                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::AssetOverflow))
633            };
634
635            // Recompute price per share (bias)
636            let Some(new_bias) = T::Rational::checked_from_rational(new_effective, issued) else {
637                return <Out as FromTag::<_, Mint>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
638            };
639
640            // Apply state updates
641            if let Err(e) = balance::set_effective::<T>(balance, new_effective) {
642                return <Out as FromTag::<_, Mint>>::from_tag(Err(e))
643            };
644
645            balance::set_bias::<T>(balance, new_bias);
646
647            // Price change boundary -> update checkpoint (Mint/Reap only)
648            if let Err(e) = balance_checkpoint::<T>(balance) {
649                return <Out as FromTag::<_, Mint>>::from_tag(Err(e))
650            }
651
652            <Out as FromTag::<_, Mint>>::from_tag(Ok(asset))
653        }
654    );
655
656    plugin_model!(
657        /// [`CanMint`] plugin family's child model over the
658        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
659        ///
660        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::CanMint`].
661        ///
662        /// ## Overview
663        ///
664        /// Performs pre-checks to determine whether minting is allowed,
665        /// without mutating balance state.
666        ///
667        /// ## Validation Rules
668        ///
669        /// - zero-value mint is disallowed (although execution treats it as no-op)
670        /// - balance must not be fresh (must be initialized)
671        /// - balance must have existing deposits (`issued > 0`)
672        /// - addition to `effective` must not overflow
673        ///
674        /// ## Semantics
675        ///
676        /// This mirrors [`ModelMint`] constraints without applying state changes,
677        /// ensuring mint execution will succeed if validation passes.
678        name: pub ModelCanMint,
679        input: In,
680        output: Out,
681        others: ['a, T, ],
682        context: ShareBalanceContext<T>,
683        bounds : [
684            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
685            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
686            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
687            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
688        ],
689        compute: |input, _context| {
690            let Ok((balance, _variant, _id, asset, _subject)) = TryIntoTag::<_, CanMint>::try_into_tag(input) else {
691                return <Out as FromTag::<_, CanMint>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
692            };
693
694            // Zero mint - invalid (execution treats as no-op, validation rejects)
695            if asset.is_zero() {
696                return <Out as FromTag::<_, CanMint>>::from_tag(Err(ShareBalanceError::ZeroAdjustmentNotAllowed))
697            }
698
699            let Some(issued) = balance::issued::<T>(&balance) else {
700                // balance must be initialized via deposit
701                return <Out as FromTag::<_, CanMint>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit))
702            };
703
704            let Some(effective) = balance::effective::<T>(&balance) else {
705                // balance must be initialized via deposit
706                return <Out as FromTag::<_, CanMint>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit))
707            };
708
709            // Mint requires existing shares (cannot bootstrap)
710            if issued.is_zero() {
711                return <Out as FromTag::<_, CanMint>>::from_tag(Err(ShareBalanceError::RequiresExistingDeposits))
712            };
713
714            // Ensure addition does not overflow effective balance
715            if effective.checked_add(&asset).is_none() {
716                return <Out as FromTag::<_, CanMint>>::from_tag(
717                    Err(ShareBalanceError::AssetOverflow)
718                )
719            }
720
721            <Out as FromTag::<_, CanMint>>::from_tag(Ok(()))
722        }
723    );
724
725    plugin_model!(
726        /// [`Reap`] plugin family's child model over the
727        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
728        ///
729        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::Reap`].
730        ///
731        /// ## Overview
732        ///
733        /// Removes value from the balance without modifying issued shares.
734        ///
735        /// This decreases `effective` while keeping `issued` constant,
736        /// thereby decreasing `bias` (value per share).
737        ///
738        /// ## Effect
739        ///
740        /// - `effective -= asset`
741        /// - `issued` remains unchanged
742        /// - `bias = effective / issued` is recomputed
743        ///
744        /// This proportionally reduces the value of all existing shares
745        /// implicitly, which is lazily reflected during withdrawal.
746        ///
747        /// ## Full Drain
748        ///
749        /// If `effective` becomes zero:
750        /// - `bias` is set to zero
751        /// - `drainpoint` (time-reference) is recorded for optimized withdrawals
752        ///
753        /// This marks the balance as fully drained. Deposits are disallowed
754        /// in this state until new value is introduced via minting. In this
755        /// state withdrawals shall be simply zero valued until minted further.
756        ///
757        /// ## Constraints
758        ///
759        /// - zero-value reap is a no-op
760        /// - balance must not be fresh (must be initialized)
761        /// - balance must have existing deposits (`issued > 0`)
762        /// - subtraction from `effective` must not underflow
763        ///
764        /// ## Semantics
765        ///
766        /// Reaping represents value removal:
767        /// - no shares are burned
768        /// - all existing receipts lose value proportionally lazily.
769        ///
770        /// Withdrawals ensures correct lazy resolution for receipts across drained
771        /// states.
772        name: pub ModelReap,
773        input: In,
774        output: Out,
775        others: ['a, T, ],
776        context: ShareBalanceContext<T>,
777        bounds : [
778            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
779            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
780            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
781            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
782        ],
783        compute: |input, _context| {
784            let Ok((mut balance, _variant, _id, asset, _subject)) = TryIntoTag::<_, Reap>::try_into_tag(input) else {
785                return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
786            };
787
788            // Zero reap -> no-op
789            if asset.is_zero() {
790                return <Out as FromTag::<_, Reap>>::from_tag(Ok(Cow::Owned(Zero::zero())));
791            }
792
793            let balance = &mut balance;
794
795            let Some(effective) = balance::effective::<T>(balance) else {
796                // balance must be initialized via deposit
797                return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
798            };
799
800            let Some(issued) = balance::issued::<T>(balance) else {
801                // balance must be initialized via deposit
802                return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
803            };
804
805            // Reap requires existing shares (cannot operate on empty balance)
806            if issued.is_zero() {
807                return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::RequiresExistingDeposits))
808            };
809
810            // Decrease effective balance
811            let Some(new_effective) = effective.checked_sub(&asset) else {
812                return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::AssetUnderflow))
813            };
814
815            // Apply new effective value
816            if let Err(e) = balance::set_effective::<T>(balance, new_effective) {
817                return <Out as FromTag::<_, Reap>>::from_tag(Err(e))
818            };
819
820            match new_effective.is_zero() {
821                true => {
822                    // Fully drained -> invalidate pricing (bias = 0)
823                    let zero_bias = Zero::zero();
824                    balance::set_bias::<T>(balance, zero_bias);
825                },
826                false => {
827                    // Recompute price per share (bias)
828                    let Some(new_bias) = T::Rational::checked_from_rational(new_effective, issued) else {
829                        return <Out as FromTag::<_, Reap>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
830                    };
831
832                    balance::set_bias::<T>(balance, new_bias);
833                }
834            };
835
836            // Handle lifecycle transition (drain boundary if reached)
837            if let Err(e) = balance_checkpoint::<T>(balance){
838                return <Out as FromTag::<_, Reap>>::from_tag(Err(e))
839            }
840
841            <Out as FromTag::<_, Reap>>::from_tag(Ok(asset))
842        }
843    );
844
845    plugin_model!(
846        /// [`CanReap`] plugin family's child model over the
847        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
848        ///
849        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::CanReap`].
850        ///
851        /// ## Overview
852        ///
853        /// Performs pre-checks to determine whether value can be removed
854        /// from the balance, without mutating state.
855        ///
856        /// ## Validation Rules
857        ///
858        /// - zero-value reap is dis-allowed although
859        /// actual operation treats it as no-op
860        /// - balance must not be fresh (must be initialized)
861        /// - balance must have existing deposits (`issued > 0`)
862        /// - subtraction from `effective` must not underflow
863        ///
864        /// ## Semantics
865        ///
866        /// This mirrors [`ModelReap`] constraints without applying state changes,
867        /// ensuring reap execution will succeed if validation passes.
868        name: pub ModelCanReap,
869        input: In,
870        output: Out,
871        others: ['a, T, ],
872        context: ShareBalanceContext<T>,
873        bounds : [
874            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
875            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
876            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
877            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
878        ],
879        compute: |input, _context| {
880            let Ok((balance, _variant, _id, asset, _subject)) = TryIntoTag::<_, CanReap>::try_into_tag(input) else {
881                return <Out as FromTag::<_, CanReap>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
882            };
883
884            // Zero reap -> invalid (execution treats as no-op, validation rejects)
885            if asset.is_zero() {
886                return <Out as FromTag::<_, CanReap>>::from_tag(Err(ShareBalanceError::ZeroAdjustmentNotAllowed))
887            }
888
889            let Some(issued) = balance::issued::<T>(&balance) else {
890                // balance must be initialized via deposit
891                return <Out as FromTag::<_, CanReap>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit))
892            };
893
894            // Reap requires existing shares (cannot bootstrap)
895            if issued.is_zero() {
896                return <Out as FromTag::<_, CanReap>>::from_tag(Err(ShareBalanceError::RequiresExistingDeposits))
897            };
898
899            let Some(effective) = balance::effective::<T>(&balance) else {
900                // balance must be initialized via deposit
901                return <Out as FromTag::<_, CanReap>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit))
902            };
903
904            // Ensure subtraction does not underflow effective balance
905            if effective.checked_sub(&asset).is_none() {
906                return <Out as FromTag::<_, CanReap>>::from_tag(
907                    Err(ShareBalanceError::AssetUnderflow)
908                )
909            }
910            <Out as FromTag::<_, CanReap>>::from_tag(Ok(()))
911        }
912    );
913
914    plugin_model!(
915        /// [`Withdraw`] plugin family's child model over the
916        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
917        ///
918        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::Withdraw`].
919        ///
920        /// ## Overview
921        ///
922        /// Resolves a [`LazyBalance::Receipt`] into a concrete asset value
923        /// and updates the balance state accordingly.
924        ///
925        /// This burns shares (`issued`) and reduces `effective`, returning
926        /// the derived value to the caller.
927        ///
928        /// ## Derivation
929        ///
930        /// Withdrawal value is computed relative to:
931        ///
932        /// - receipt's `shares`
933        /// - receipt's `bias` (at deposit time)
934        /// - current balance `bias` (price per share)
935        ///
936        /// ```text
937        /// value = shares * receipt_bias * (current_bias / receipt_bias) // or
938        ///       = shares * current_bias // if balance drained after deposit hence receipt is outdated
939        /// ```
940        ///
941        /// Although the formula simplifies to `shares * current_bias` if
942        /// the receipt gets outdated due to a recent drain, the stored
943        /// `receipt_bias` is required to correctly handle:
944        /// - drain scenarios
945        /// - checkpoint-based invalidation
946        /// - historical pricing context
947        ///
948        /// Special handling:
949        ///
950        /// - if balance was drained after receipt checkpoint:
951        ///   - receipt bias is reset (treated as 1:1)
952        ///   - shares map directly to value
953        ///
954        /// ## Effect
955        ///
956        /// - `effective -= withdraw`
957        /// - `issued -= shares`
958        ///
959        /// ## Edge Cases
960        ///
961        /// - withdrawal is capped at `effective`
962        /// - full withdrawal resets or reinitializes balance
963        ///
964        /// ## Semantics
965        ///
966        /// Withdrawal represents **lazy resolution of ownership**:
967        ///
968        /// - receipts encode relative claim
969        /// - value is derived at execution time
970        /// - drained states are handled via checkpoint logic
971        name: pub ModelWithdraw,
972        input: In,
973        output: Out,
974        others: ['a, T],
975        context: ShareBalanceContext<T>,
976        bounds : [
977            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
978            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
979            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
980            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
981            T::Asset: From<<T::Rational as FixedPointNumber>::Inner>,
982        ],
983        compute: |input, _context| {
984            let Ok((mut balance, variant, id, receipt)) = TryIntoTag::<_, Withdraw>::try_into_tag(input) else {
985                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
986            };
987
988            let balance = &mut balance;
989
990            let Some(effective) = balance::effective::<T>(balance) else {
991                // balance must be initialized via deposit
992                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
993            };
994
995            let Some(issued) = balance::issued::<T>(balance) else {
996                // balance must be initialized via deposit
997                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
998            };
999
1000            let Some(shares) = receipt::shares::<T>(&receipt) else {
1001                // invalid receipt structure
1002                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1003            };
1004
1005            // Invalid receipt obvious cases
1006            if shares > issued || shares.is_zero() || issued.is_zero() {
1007                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1008            }
1009
1010            let Some(mut receipt_bias) = receipt::bias::<T>(&receipt) else {
1011                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1012            };
1013
1014            // Base value at deposit time: shares * receipt_bias
1015            let Some(mut value_fixed) = T::Rational::saturating_from_integer(shares).checked_mul(&receipt_bias) else {
1016                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
1017            };
1018
1019            let Some(checkpoint) = receipt::checkpoint::<T>(&receipt) else {
1020                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1021            };
1022
1023            let Some(drainpoint) = balance::drainpoint::<T>(balance) else {
1024                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
1025            };
1026
1027            // If balance was drained after receipt creation:
1028            // reset derivation to 1:1 (shares -> value)
1029            if drainpoint > checkpoint {
1030                // Drain invalidates historical pricing -> reset to share-only basis
1031                value_fixed = T::Rational::saturating_from_integer(shares);
1032                receipt_bias = One::one();
1033            };
1034
1035            let Some(bias) = balance::bias::<T>(balance) else {
1036                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
1037            };
1038
1039            // Compute relative price change since deposit
1040            let Some(final_ratio) = bias.checked_div(&receipt_bias) else {
1041                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
1042            };
1043
1044            // Apply ratio to derive final value
1045            let Some(final_value_fixed) = value_fixed.checked_mul(&final_ratio) else {
1046                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::InadequatePrecision))
1047            };
1048
1049            // Convert from fixed-point -> asset
1050            let Some(withdraw_fixed) = final_value_fixed.into_inner().checked_div(&T::Rational::DIV) else {
1051                // unreachable unless DIV is zero
1052                debug_assert!(
1053                    false,
1054                    "divide by zero during fixed-point scaling during \
1055                    ShareBalanceFamily::Withdraw for id {:?}, variant {:?} \
1056                    receipt {:?}",
1057                    variant, id, receipt,
1058                );
1059                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::FixedPointScalingFailed))
1060            };
1061
1062            // Cap withdrawal to available effective balance
1063            let withdraw = Into::<T::Asset>::into(withdraw_fixed).min(effective);
1064
1065            // Apply state updates
1066            let Some(new_effective) = effective.checked_sub(&withdraw) else {
1067                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::AssetUnderflow))
1068            };
1069
1070            let Some(new_issued) = issued.checked_sub(&shares) else {
1071                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(ShareBalanceError::SharesUnderflow))
1072            };
1073
1074            if let Err(e) = balance::set_effective::<T>(balance, new_effective) {
1075                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(e))
1076            };
1077
1078            if let Err(e) = balance::set_issued::<T>(balance, new_issued) {
1079                return <Out as FromTag::<_, Withdraw>>::from_tag(Err(e))
1080            };
1081
1082            // Handle terminal states
1083            if new_issued.is_zero() {
1084
1085                if !new_effective.is_zero() {
1086                    // leftover value -> reset balance and return all
1087                    if let Err(e) = balance::init_balance::<T>(balance) {
1088                        return <Out as FromTag::<_, Withdraw>>::from_tag(Err(e))
1089                    };
1090
1091                    // Last shareholder -> receives full remaining balance i.e., dusts
1092                    return <Out as FromTag::<_, Withdraw>>::from_tag(
1093                        Ok(Cow::Owned(withdraw.saturating_add(new_effective)))
1094                    )
1095                }
1096
1097                // fully empty -> reset pricing
1098                balance::set_bias::<T>(balance, One::one());
1099            }
1100
1101            <Out as FromTag::<_, Withdraw>>::from_tag(Ok(Cow::Owned(withdraw)))
1102        }
1103    );
1104
1105    plugin_model!(
1106        /// [`CanWithdraw`] plugin family's child model over the
1107        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1108        ///
1109        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::CanWithdraw`].
1110        ///
1111        /// ## Overview
1112        ///
1113        /// Performs pre-checks to determine whether a receipt can be
1114        /// successfully withdrawn, without mutating balance state.
1115        ///
1116        /// ## Validation Rules
1117        ///
1118        /// - balance must be initialized (must have issued supply)
1119        /// - receipt must be structurally valid
1120        /// - receipt must carry:
1121        ///   - `shares` (ownership)
1122        ///   - `bias` (pricing context at deposit)
1123        ///   - `checkpoint` (time anchor)
1124        /// - `shares` must be non-zero
1125        /// - `issued` must be non-zero
1126        /// - receipt cannot claim more shares than total issued
1127        ///
1128        /// ## Semantics
1129        ///
1130        /// This mirrors [`ModelWithdraw`] constraints without applying state changes,
1131        /// ensuring withdrawal execution will succeed if validation passes.
1132        ///
1133        /// A valid receipt represents a **bounded ownership claim** over the
1134        /// current balance, which can be safely resolved at execution time.
1135        name: pub ModelCanWithdraw,
1136        input: In,
1137        output: Out,
1138        others: ['a, T, ],
1139        context: ShareBalanceContext<T>,
1140        bounds : [
1141            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1142            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1143            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1144            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1145        ],
1146        compute: |input, _context| {
1147            let Ok((balance, _variant, _id, receipt)) = TryIntoTag::<_, CanWithdraw>::try_into_tag(input) else {
1148                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1149            };
1150
1151            let Some(issued) = balance::issued::<T>(&balance) else {
1152                // balance must be initialized via deposit
1153                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
1154            };
1155
1156            let Some(shares) =  receipt::shares::<T>(&receipt) else {
1157                // invalid receipt structure
1158                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1159            };
1160
1161            // receipt must carry pricing context (bias at deposit time)
1162            if receipt::bias::<T>(&receipt).is_none() {
1163                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1164            }
1165
1166            // receipt must carry time anchor (checkpoint)
1167            if receipt::checkpoint::<T>(&receipt).is_none() {
1168                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1169            }
1170
1171            // Validate ownership claim:
1172            // - shares must be non-zero
1173            // - issued supply must exist
1174            // - receipt cannot claim more shares than total issued
1175            if shares > issued || shares.is_zero() || issued.is_zero() {
1176                return <Out as FromTag::<_, CanWithdraw>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1177            }
1178
1179            <Out as FromTag::<_, CanWithdraw>>::from_tag(Ok(()))
1180        }
1181    );
1182
1183    plugin_model!(
1184        /// [`TotalValue`] plugin family's child model over the
1185        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1186        ///
1187        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::TotalValue`].
1188        ///
1189        /// ## Overview
1190        ///
1191        /// Returns the total effective value currently held by the balance.
1192        ///
1193        /// This represents the aggregate value backing all issued shares,
1194        /// independent of any individual receipt.
1195        ///
1196        /// ## Semantics
1197        ///
1198        /// - `effective` reflects the current total value of the balance
1199        /// - includes all value changes from:
1200        ///   - deposits
1201        ///   - mint (value injection)
1202        ///   - reap (value removal)
1203        ///
1204        /// If the balance is uninitialized (fresh), the total value is treated as zero.
1205        ///
1206        /// This operation does not depend on receipts and does not mutate state.
1207        name: pub ModelTotalValue,
1208        input: In,
1209        output: Out,
1210        others: ['a, T, ],
1211        context: ShareBalanceContext<T>,
1212        bounds : [
1213            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1214            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1215            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1216            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1217        ],
1218        compute: |input, _context| {
1219            let Ok((balance, _variant, _id)) =  TryIntoTag::<_, TotalValue>::try_into_tag(input) else {
1220                return <Out as FromTag::<_, TotalValue>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1221            };
1222
1223            let Some(effective) = balance::effective::<T>(&balance) else {
1224                // fresh balance -> no value accumulated
1225                return <Out as FromTag::<_, TotalValue>>::from_tag(Ok(Cow::Owned(Zero::zero())))
1226            };
1227
1228            // return current aggregate value backing all shares
1229            <Out as FromTag::<_, TotalValue>>::from_tag(Ok(Cow::Owned(effective)))
1230        }
1231    );
1232
1233    plugin_model!(
1234        /// [`ReceiptActiveValue`] plugin family's child model over the
1235        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1236        ///
1237        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::ReceiptActiveValue`].
1238        ///
1239        /// ## Overview
1240        ///
1241        /// Computes the current redeemable value of a receipt without mutating
1242        /// the original balance state.
1243        ///
1244        /// This simulates a withdrawal using a cloned balance, returning the
1245        /// value that would be obtained if the receipt were withdrawn now.
1246        ///
1247        /// ## Semantics
1248        ///
1249        /// - performs full [`CanWithdraw`] validation before derivation
1250        /// - executes [`ModelWithdraw`] on a cloned balance
1251        /// - preserves original state (read-only evaluation)
1252        ///
1253        /// The returned value reflects:
1254        ///
1255        /// - current `bias` (price per share)
1256        /// - receipt's `shares`
1257        /// - checkpoint / drainpoint adjustments
1258        ///
1259        /// ## Guarantees
1260        ///
1261        /// - no mutation of original balance
1262        /// - consistent with [`ModelWithdraw`] execution
1263        /// - safe preview of withdrawal outcome
1264        ///
1265        /// This acts as a **pure evaluation layer** over the withdrawal logic.
1266        name: pub ModelReceiptActiveValue,
1267        input: In,
1268        output: Out,
1269        others: ['a, T, ],
1270        context: ShareBalanceContext<T>,
1271        bounds : [
1272            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1273            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1274            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1275            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1276        ],
1277        compute: |input, _context| {
1278           let Ok((balance, variant, id, receipt)) =  TryIntoTag::<_, ReceiptActiveValue>::try_into_tag(input) else {
1279                return <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1280            };
1281
1282            // Validate withdrawal feasibility using a cloned balance
1283            let can_withdraw_input = <In as FromTag::<_, CanWithdraw>>::from_tag(
1284                (
1285                    Cow::Owned((*balance).clone()),
1286                    variant.clone(),
1287                    id.clone(),
1288                    receipt.clone()
1289                )
1290            );
1291
1292            let raw = T::can_withdraw(can_withdraw_input);
1293
1294            let Ok(result) =  TryIntoTag::<_, CanWithdraw>::try_into_tag(raw) else {
1295                return <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1296            };
1297
1298            // Propagate validation failure
1299            if let Err(e) = result {
1300                return <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Err(e));
1301            }
1302
1303            // Simulate withdrawal on a cloned balance (no state mutation)
1304            let withdraw_input = <In as FromTag::<_, Withdraw>>::from_tag(
1305                (
1306                    MutHandle::Owned((*balance).clone()),
1307                    variant,
1308                    id,
1309                    receipt
1310                )
1311            );
1312
1313            let raw = T::withdraw(withdraw_input);
1314
1315            let Ok(result) =  TryIntoTag::<_, Withdraw>::try_into_tag(raw) else {
1316                return <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1317            };
1318
1319            // Return derived value or error from withdrawal simulation
1320            match result {
1321                Ok(v) => <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Ok(v)),
1322                Err(e) => <Out as FromTag::<_, ReceiptActiveValue>>::from_tag(Err(e)),
1323            }
1324        }
1325    );
1326    plugin_model!(
1327        /// [`Drain`] plugin family's child model over the
1328        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1329        ///
1330        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::Drain`].
1331        ///
1332        /// ## Overview
1333        ///
1334        /// Fully removes all effective value from the balance in a single operation.
1335        ///
1336        /// This is implemented as a composition of:
1337        ///
1338        /// - [`ModelTotalValue`]: derive current total value
1339        /// - [`ModelReap`]: remove that value from the balance
1340        ///
1341        /// ## Effect
1342        ///
1343        /// - `effective -> 0`
1344        /// - `issued` remains unchanged
1345        /// - `bias -> 0` (balance enters drained state)
1346        /// - `drainpoint` is recorded via checkpoint logic
1347        ///
1348        /// ## Semantics
1349        ///
1350        /// Drain represents a **complete value removal**:
1351        ///
1352        /// - all shares lose value (price per share becomes zero)
1353        /// - no shares are burned
1354        /// - receipts remain valid but resolve to zero until mint
1355        ///
1356        /// This transitions the balance into the **drained state**.
1357        ///
1358        /// ## Guarantees
1359        ///
1360        /// - deterministic: always removes full value
1361        /// - equivalent to `Reap(effective)`
1362        /// - respects all [`ModelReap`] invariants
1363        ///
1364        /// This acts as a **convenience operation** for full balance
1365        /// depletion (a very unpractical edge case for production systems visited).
1366        name: pub ModelDrain,
1367        input: In,
1368        output: Out,
1369        others: ['a, T, ],
1370        context: ShareBalanceContext<T>,
1371        bounds : [
1372            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1373            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1374            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1375            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1376        ],
1377        compute: |input, _context| {
1378           let Ok((balance, variant, id)) =  TryIntoTag::<_, Drain>::try_into_tag(input) else {
1379                return <Out as FromTag::<_, Drain>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1380            };
1381
1382            // Clone balance to safely derive total value without mutation
1383            let b = (*balance).clone();
1384
1385            // Compute current total value backing all shares
1386            let total_value_input = <In as FromTag::<_, TotalValue>>::from_tag(
1387                (
1388                    Cow::Owned(b),
1389                    variant.clone(),
1390                    id.clone(),
1391                )
1392            );
1393
1394            let raw = T::total_value(total_value_input);
1395
1396            let Ok(result) =  TryIntoTag::<_, TotalValue>::try_into_tag(raw) else {
1397                return <Out as FromTag::<_, Drain>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1398            };
1399
1400            // Propagate total value derivation result
1401            let value = match result {
1402                Ok(v) => v,
1403                Err(e) => return <Out as FromTag::<_, Drain>>::from_tag(Err(e)),
1404            };
1405
1406            // Remove entire value via reap (force exact execution)
1407            let reap_input = <In as FromTag::<_, Reap>>::from_tag(
1408                (
1409                    balance,
1410                    variant,
1411                    id,
1412                    value,
1413                    Cow::Owned(Directive::new(Precision::Exact, Fortitude::Force))
1414                )
1415            );
1416
1417            let raw = T::reap(reap_input);
1418
1419            let Ok(result) =  TryIntoTag::<_, Reap>::try_into_tag(raw) else {
1420                return <Out as FromTag::<_, Drain>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1421            };
1422
1423            // Return result of full reap
1424            match result {
1425                Ok(v) => <Out as FromTag::<_, Drain>>::from_tag(Ok(v)),
1426                Err(e) => <Out as FromTag::<_, Drain>>::from_tag(Err(e)),
1427            }
1428        }
1429    );
1430
1431    plugin_model!(
1432        /// [`HasDeposits`] plugin family's child model over the
1433        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1434        ///
1435        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::HasDeposits`].
1436        ///
1437        /// ## Overview
1438        ///
1439        /// Checks whether the balance currently has active deposits
1440        /// (i.e., issued shares exist).
1441        ///
1442        /// ## Validation Rules
1443        ///
1444        /// - balance must not be fresh (must be initialized)
1445        /// - balance must have issued shares (`issued > 0`)
1446        ///
1447        /// ## Semantics
1448        ///
1449        /// - `issued > 0` -> balance has active deposits
1450        /// - `issued == 0` -> balance has been fully withdrawn
1451        ///
1452        /// This distinguishes:
1453        ///
1454        /// - fresh balance (never initialized)
1455        /// - active balance (has deposits)
1456        /// - fully withdrawn balance (no remaining shares)
1457        ///
1458        /// This operation does not mutate state.
1459        name: pub ModelHasDeposits,
1460        input: In,
1461        output: Out,
1462        others: ['a, T, ],
1463        context: ShareBalanceContext<T>,
1464        bounds : [
1465            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1466            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1467            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1468            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1469        ],
1470        compute: |input, _context| {
1471            let Ok((balance, _variant, _id)) =  TryIntoTag::<_, HasDeposits>::try_into_tag(input) else {
1472                return <Out as FromTag::<_, HasDeposits>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1473            };
1474
1475            // Fresh balance -> no deposits have ever been made
1476            if *balance == Default::default() {
1477                return <Out as FromTag::<_, HasDeposits>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
1478            };
1479
1480            let Some(issued) = balance::issued::<T>(&balance) else {
1481                // unreachable unless balance virtual fields are corrupted
1482                return <Out as FromTag::<_, HasDeposits>>::from_tag(Err(ShareBalanceError::BalanceNotInitiatedViaDeposit));
1483            };
1484
1485            // No issued shares -> balance fully withdrawn
1486            if issued.is_zero() {
1487                return <Out as FromTag::<_, HasDeposits>>::from_tag(Err(ShareBalanceError::AllDepositsWithdrawn));
1488            }
1489
1490            // Active deposits exist
1491            <Out as FromTag::<_, HasDeposits>>::from_tag(Ok(()))
1492        }
1493    );
1494
1495    plugin_model!(
1496        /// [`ReceiptDepositValue`] plugin family's child model over the
1497        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1498        ///
1499        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::ReceiptDepositValue`].
1500        ///
1501        /// ## Overview
1502        ///
1503        /// Returns the original deposited value associated with a receipt.
1504        ///
1505        /// This reflects the principal amount supplied at deposit time,
1506        /// independent of any subsequent balance state changes.
1507        ///
1508        /// ## Semantics
1509        ///
1510        /// - corresponds to receipt's `principal`
1511        /// - does not depend on current `bias` (price per share)
1512        /// - does not account for mint/reap effects
1513        ///
1514        /// This represents the **initial contribution**, not the current value.
1515        ///
1516        /// ## Guarantees
1517        ///
1518        /// - pure read (no state mutation)
1519        /// - invariant across all balance transitions
1520        /// - independent of withdrawal logic
1521        ///
1522        /// This acts as a **historical reference value** for the receipt.
1523        name: pub ModelReceiptDepositValue,
1524        input: In,
1525        output: Out,
1526        others: ['a, T, ],
1527        context: ShareBalanceContext<T>,
1528        bounds : [
1529            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1530            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1531            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1532            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1533        ],
1534        compute: |input, _context| {
1535            let Ok(receipt) =  TryIntoTag::<_, ReceiptDepositValue>::try_into_tag(input) else {
1536                return <Out as FromTag::<_, ReceiptDepositValue>>::from_tag(Err(ShareBalanceError::InvalidPluginParams));
1537            };
1538
1539            let Some(deposit) = receipt::principal::<T>(&receipt) else {
1540                // invalid receipt structure
1541                return <Out as FromTag::<_, ReceiptDepositValue>>::from_tag(Err(ShareBalanceError::InvalidReceipt))
1542            };
1543
1544            // return original deposited value (principal)
1545            <Out as FromTag::<_, ReceiptDepositValue>>::from_tag(Ok(Cow::Owned(deposit)))
1546        }
1547    );
1548
1549    plugin_model!(
1550        /// [`DepositLimits`] plugin family's child model over the
1551        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1552        ///
1553        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::DepositLimits`].
1554        ///
1555        /// ## Overview
1556        ///
1557        /// Provides deposit constraints for the balance.
1558        ///
1559        /// ## Semantics
1560        ///
1561        /// This implementation defines **no limits**:
1562        ///
1563        /// - deposits are fully permissive
1564        /// - no min/max bounds are enforced
1565        ///
1566        /// The balance operates purely on a share-based model,
1567        /// where value distribution is determined by `bias`
1568        /// (price per share).
1569        ///
1570        /// ## Notes
1571        ///
1572        /// - no safeguards against extreme deposits
1573        /// - relies on external discipline or higher-level controls (callers)
1574        ///
1575        /// Returns default (unbounded) limits.
1576        name: pub ModelDepositLimits,
1577        input: In,
1578        output: Out,
1579        others: ['a, T, ],
1580        context: ShareBalanceContext<T>,
1581        bounds : [
1582            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1583            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1584            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1585            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1586        ],
1587        compute: |_input, _context| {
1588            // No limits -> return default (unbounded)
1589            <Out as FromTag::<_, DepositLimits>>::from_tag(Ok(Cow::Owned(Default::default())))
1590        }
1591    );
1592
1593    plugin_model!(
1594        /// [`MintLimits`] plugin family's child model over the
1595        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1596        ///
1597        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::MintLimits`].
1598        ///
1599        /// ## Overview
1600        ///
1601        /// Provides mint constraints for the balance.
1602        ///
1603        /// ## Semantics
1604        ///
1605        /// This implementation defines **no limits**:
1606        ///
1607        /// - mint operations are fully permissive
1608        /// - no bounds on value injection
1609        ///
1610        /// Mint directly affects `bias` (price per share),
1611        /// increasing value across all existing shares.
1612        ///
1613        /// ## Notes
1614        ///
1615        /// - unrestricted minting can skew share price
1616        /// - value distribution may become imbalanced
1617        ///
1618        /// Returns default (unbounded) limits.
1619        name: pub ModelMintLimits,
1620        input: In,
1621        output: Out,
1622        others: ['a, T, ],
1623        context: ShareBalanceContext<T>,
1624        bounds : [
1625            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1626            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1627            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1628            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1629        ],
1630        compute: |_input, _context| {
1631            // No limits -> return default (unbounded)
1632            <Out as FromTag::<_, MintLimits>>::from_tag(Ok(Cow::Owned(Default::default())))
1633        }
1634    );
1635
1636    plugin_model!(
1637        /// [`ReapLimits`] plugin family's child model over the
1638        /// [`LazyBalance`]'s compile-time marker via [`ShareBalanceContext`].
1639        ///
1640        /// Bound to [`ShareBalanceFamily`] via [`LazyBalanceRoot::ReapLimits`].
1641        ///
1642        /// ## Overview
1643        ///
1644        /// Provides reap constraints for the balance.
1645        ///
1646        /// ## Semantics
1647        ///
1648        /// This implementation defines **no limits**:
1649        ///
1650        /// - reap operations are fully permissive
1651        /// - no bounds on value removal
1652        ///
1653        /// Reap directly affects `bias` (price per share),
1654        /// decreasing value across all existing shares.
1655        ///
1656        /// ## Notes
1657        ///
1658        /// - unrestricted reaping can distort share pricing
1659        /// - combined misuse of mint/reap may skew withdrawal distribution
1660        ///
1661        /// Returns default (unbounded) limits.
1662        name: pub ModelReapLimits,
1663        input: In,
1664        output: Out,
1665        others: ['a, T, ],
1666        context: ShareBalanceContext<T>,
1667        bounds : [
1668            T: LazyBalance<Input<'a> = In, Output<'a> = Out>,
1669            Context<T>: VirtualError<LazyBalanceError, Error = ShareBalanceError>,
1670            Out: LazyBalanceOutput<'a, T::Asset, T::Receipt, T::SnapShot, T::Time, T::Limits, T>,
1671            In: LazyBalanceInput<'a, T::Balance, T::Variant, T::Id, T::Asset, T::Receipt, T>,
1672        ],
1673        compute: |_input, _context| {
1674            // No limits -> return default (unbounded)
1675            <Out as FromTag::<_, ReapLimits>>::from_tag(Ok(Cow::Owned(Default::default())))
1676        }
1677    );
1678
1679    // ===============================================================================
1680    // ````````````````````````````` VIRTUAL STRUCTURES ``````````````````````````````
1681    // ===============================================================================
1682
1683    /// Balance-level accessors and initialization utilities.
1684    ///
1685    /// Provides a field-oriented interface over [`LazyBalance::Balance`],
1686    /// treating it as a **virtual struct** composed via discriminants.
1687    ///
1688    /// ## Logical Structure
1689    ///
1690    /// ```ignore
1691    /// struct <T as LazyBalance>::Balance {
1692    ///     BalanceAsset.0: T::Asset,       // effective
1693    ///     BalanceAsset.1: T::Asset,       // issued
1694    ///     BalanceRational: T::Rational,   // bias
1695    ///     BalanceTime.0: T::Time,         // checkpoint
1696    ///     BalanceTime.1: T::Time,         // drainpoint
1697    /// }
1698    /// ```
1699    ///
1700    /// - discriminants = field identifiers
1701    /// - `.0`, `.1` = multiple values (`Many`)
1702    ///
1703    /// ```ignore
1704    /// BalanceAsset    => Many(T::Asset)
1705    /// BalanceRational => Some(T::Rational)
1706    /// BalanceTime     => Many(T::Time)
1707    /// ```
1708    ///
1709    /// This is a **type projection**:
1710    /// - `T` defines the schema
1711    /// - storage is discriminant-keyed and resolved via [`VirtualDynField`]
1712    /// - this decouples logical structure from storage layout.
1713    ///
1714    /// ## Semantics
1715    ///
1716    /// - **Asset**: `effective`, `issued`
1717    /// - **Rational**: `bias`
1718    /// - **Time**: `checkpoint`, `drainpoint`
1719    ///
1720    /// ## Initialization
1721    ///
1722    /// [`balance::is_fresh_balance`] lazily initializes:
1723    ///
1724    /// - `effective = 0`, `issued = 0`
1725    /// - `bias = 1`
1726    /// - `checkpoint = 0`, `drainpoint = 0`
1727    ///
1728    /// ## Context
1729    ///
1730    /// [`ShareBalanceContext`] supplies:
1731    /// - bounds ([`VirtualDynBound`])
1732    /// - empty extensions ([`empty_virtual_extension!`](frame_suite::empty_virtual_extension))
1733    ///
1734    /// Structure stays abstract (but here implemented concretely);
1735    /// layout and limits come from the context.
1736    mod balance {
1737        use super::*;
1738
1739        /// Returns the current effective value of the balance.
1740        ///
1741        /// [`LazyBalance::Balance`] virtual field: `effective`
1742        ///
1743        /// Internally resolved from the discriminant [`BalanceAsset`] field at index `0`.
1744        pub fn effective<T: LazyBalance>(balance: &T::Balance) -> Option<T::Asset> {
1745            <T::Balance as DynFieldHelpers<BalanceAsset>>::index_get(balance, 0)
1746        }
1747
1748        /// Sets the current effective value of the balance.
1749        ///
1750        /// [`LazyBalance::Balance`] virtual field: `effective`
1751        ///
1752        /// Writes to the discriminant [`BalanceAsset`] field at index `0`.
1753        pub fn set_effective<T: LazyBalance>(
1754            balance: &mut T::Balance,
1755            value: T::Asset,
1756        ) -> Result<(), ShareBalanceError> {
1757            <T::Balance as DynFieldHelpers<BalanceAsset>>::index_set(balance, 0, value)
1758                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
1759        }
1760
1761        /// Returns the base value (total shares) backing the balance.
1762        ///
1763        /// [`LazyBalance::Balance`] virtual field: `issued`
1764        ///
1765        /// Internally resolved from the discriminant [`BalanceAsset`] field at index `1`.
1766        pub fn issued<T: LazyBalance>(balance: &T::Balance) -> Option<T::Asset> {
1767            <T::Balance as DynFieldHelpers<BalanceAsset>>::index_get(balance, 1)
1768        }
1769
1770        /// Sets the base value (total shares) of the balance.
1771        ///
1772        /// [`LazyBalance::Balance`] virtual field: `issued`
1773        ///
1774        /// Writes to the discriminant [`BalanceAsset`] field at index `1`.
1775        pub fn set_issued<T: LazyBalance>(
1776            balance: &mut T::Balance,
1777            value: T::Asset,
1778        ) -> Result<(), ShareBalanceError> {
1779            <T::Balance as DynFieldHelpers<BalanceAsset>>::index_set(balance, 1, value)
1780                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
1781        }
1782
1783        /// Returns the scaling factor (share-price) applied to the balance value.
1784        ///
1785        /// [`LazyBalance::Balance`] virtual field: `bias`
1786        ///
1787        /// Internally resolved from the discriminant [`BalanceRational`] field.
1788        pub fn bias<T: LazyBalance>(balance: &T::Balance) -> Option<T::Rational> {
1789            <T::Balance as DynFieldHelpers<BalanceRational>>::get(balance)
1790        }
1791
1792        /// Sets the scaling factor (share-price) applied to the balance value.
1793        ///
1794        /// [`LazyBalance::Balance`] virtual field: `bias`
1795        ///
1796        /// Writes to the discriminant [`BalanceRational`] field.
1797        pub fn set_bias<T: LazyBalance>(balance: &mut T::Balance, value: T::Rational) {
1798            <T::Balance as DynFieldHelpers<BalanceRational>>::set(balance, value)
1799        }
1800
1801        /// Returns the most recent time at which the balance state was adjusted i.e.,
1802        /// reap or mint.
1803        ///
1804        /// [`LazyBalance::Balance`] virtual field: `checkpoint`
1805        ///
1806        /// Internally resolved from the discriminant [`BalanceTime`] field at index 0.
1807        pub fn checkpoint<T: LazyBalance>(balance: &T::Balance) -> Option<T::Time> {
1808            <T::Balance as DynFieldHelpers<BalanceTime>>::index_get(balance, 0)
1809        }
1810
1811        /// Sets the most recent adjusted (reap/mint) time of the balance.
1812        ///
1813        /// [`LazyBalance::Balance`] virtual field: `checkpoint`
1814        ///
1815        /// Writes to the discriminant [`BalanceTime`] field at index 0.
1816        pub fn set_checkpoint<T: LazyBalance>(
1817            balance: &mut T::Balance,
1818            value: T::Time,
1819        ) -> Result<(), ShareBalanceError> {
1820            <T::Balance as DynFieldHelpers<BalanceTime>>::index_set(balance, 0, value)
1821                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
1822        }
1823
1824        /// Returns the most recent time at which the balance state was drained.
1825        ///
1826        /// [`LazyBalance::Balance`] virtual field: `drainpoint`
1827        ///
1828        /// Internally resolved from the discriminant [`BalanceTime`] field at index 1.
1829        pub fn drainpoint<T: LazyBalance>(balance: &T::Balance) -> Option<T::Time> {
1830            <T::Balance as DynFieldHelpers<BalanceTime>>::index_get(balance, 1)
1831        }
1832
1833        /// Sets the most recent drained time of the balance.
1834        ///
1835        /// [`LazyBalance::Balance`] virtual field: `drainpoint`
1836        ///
1837        /// Writes to the discriminant [`BalanceTime`] field at index 1.
1838        pub fn set_drainpoint<T: LazyBalance>(
1839            balance: &mut T::Balance,
1840            value: T::Time,
1841        ) -> Result<(), ShareBalanceError> {
1842            <T::Balance as DynFieldHelpers<BalanceTime>>::index_set(balance, 1, value)
1843                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
1844        }
1845
1846        /// Initializes a [`LazyBalance::Balance`] **only if not already initialized**.
1847        pub fn is_fresh_balance<T: LazyBalance>(
1848            balance: &mut T::Balance,
1849        ) -> Result<(), ShareBalanceError> {
1850            if balance::effective::<T>(balance).is_none() {
1851                self::init_balance::<T>(balance)?;
1852            }
1853            Ok(())
1854        }
1855
1856        /// Initializes a [`LazyBalance::Balance`] forcefully.
1857        ///
1858        /// Ensures all fields are set to sensible defaults:
1859        /// - `effective` = 0
1860        /// - `issued` = 0
1861        /// - `bias` = 1
1862        /// - `checkpoint` = 0
1863        pub fn init_balance<T: LazyBalance>(
1864            balance: &mut T::Balance,
1865        ) -> Result<(), ShareBalanceError> {
1866            set_effective::<T>(balance, Zero::zero())?;
1867            set_issued::<T>(balance, Zero::zero())?;
1868            set_bias::<T>(balance, One::one());
1869            set_checkpoint::<T>(balance, Zero::zero())?;
1870            set_drainpoint::<T>(balance, Zero::zero())?;
1871            Ok(())
1872        }
1873
1874        /// [`LazyBalance::Balance`] virtual field layout for the
1875        /// [`BalanceAsset`] discriminant
1876        ///
1877        /// Allocates two asset fields:
1878        /// - `effective`  (internally index `0`)
1879        /// - `issued`  (internally index `1`)
1880        impl<T> VirtualDynBound<BalanceAsset> for ShareBalanceContext<T> {
1881            type Bound = ConstU32<2>;
1882        }
1883
1884        /// [`LazyBalance::Balance`] virtual field layout for the
1885        /// [`BalanceRational`] discriminant
1886        ///
1887        /// Allocates one rational field:
1888        /// - `bias`
1889        impl<T> VirtualDynBound<BalanceRational> for ShareBalanceContext<T> {
1890            type Bound = ConstU32<1>;
1891        }
1892
1893        /// [`LazyBalance::Balance`] virtual field layout for the
1894        /// [`BalanceTime`] discriminant
1895        ///
1896        /// Allocates two asset fields:
1897        /// - `checkpoint`  (internally index `0`)
1898        /// - `drainpoint`  (internally index `1`)
1899        impl<T> VirtualDynBound<BalanceTime> for ShareBalanceContext<T> {
1900            type Bound = ConstU32<2>;
1901        }
1902
1903        // `LazyBalance::Balance` virtual extension schema for the
1904        // `BalanceAddon` discriminant
1905        //
1906        // Defines an empty extension schema.
1907        //
1908        // Balance do not support addon-backed fields, and always behave
1909        // as having no extension data.
1910        empty_virtual_extension!(
1911            target: T::Balance,
1912            tag: BalanceAddon,
1913            schema: ShareBalanceContext<T>,
1914            generics: [T]
1915        );
1916    }
1917
1918    /// [`LazyBalance::SnapShot`] virtual field layout.
1919    ///
1920    /// This configuration defines a **zero-sized snapshot**:
1921    ///
1922    /// ```ignore
1923    /// struct <T as LazyBalance>::SnapShot {}
1924    /// ```
1925    ///
1926    /// No fields are allocated:
1927    /// - [`SnapShotAsset`]     -> 0
1928    /// - [`SnapShotRational`]  -> 0
1929    /// - [`SnapShotTime`]      -> 0
1930    ///
1931    /// ## Semantics
1932    ///
1933    /// Snapshots are **not used** in the [`ShareBalanceFamily`] model:
1934    ///
1935    /// - no historical state
1936    /// - no time-based projections
1937    /// - no additional storage
1938    ///
1939    /// The type exists only to satisfy the [`LazyBalance`] contract.
1940    ///
1941    /// ## Extension
1942    ///
1943    /// No extensions are supported:
1944    ///
1945    /// ```ignore
1946    /// empty_virtual_extension!(...)
1947    /// ```
1948    ///
1949    /// Snapshot behaves as a **pure placeholder type**.
1950    mod snapshot {
1951        use super::*;
1952
1953        impl<T> VirtualDynBound<SnapShotAsset> for ShareBalanceContext<T> {
1954            type Bound = ConstU32<0>;
1955        }
1956
1957        impl<T> VirtualDynBound<SnapShotRational> for ShareBalanceContext<T> {
1958            type Bound = ConstU32<0>;
1959        }
1960
1961        impl<T> VirtualDynBound<SnapShotTime> for ShareBalanceContext<T> {
1962            type Bound = ConstU32<0>;
1963        }
1964
1965        empty_virtual_extension!(
1966            target: T::SnapShot,
1967            tag: SnapShotAddon,
1968            schema: ShareBalanceContext<T>,
1969            generics: [T]
1970        );
1971    }
1972
1973    /// Receipt-level accessors and utilities.
1974    ///
1975    /// Provides a field-oriented interface over [`LazyBalance::Receipt`],
1976    /// treating it as a **virtual struct**
1977    /// composed via discriminants.
1978    ///
1979    /// ## Semantics
1980    ///
1981    /// A receipt represents a **claim over deposited value**:
1982    ///
1983    /// ```text
1984    /// deposit -> issue receipt
1985    /// balance mutation -> affects value
1986    /// withdraw -> resolve receipt
1987    /// ```
1988    ///
1989    /// Captures:
1990    /// - `principal`, `shares`
1991    /// - `bias`
1992    /// - `checkpoint`
1993    ///
1994    /// ## Logical Structure
1995    ///
1996    /// ```ignore
1997    /// struct <T as LazyBalance>::Receipt {
1998    ///     ReceiptAsset.0: T::Asset,       // principal
1999    ///     ReceiptAsset.1: T::Asset,       // shares
2000    ///     ReceiptRational: T::Rational,   // bias
2001    ///     ReceiptTime: T::Time,           // checkpoint
2002    /// }
2003    /// ```
2004    ///
2005    /// - discriminants = field identifiers
2006    /// - `.0`, `.1` = multiple values (`Many`)
2007    ///
2008    /// ```ignore
2009    /// ReceiptAsset    => Many(T::Asset)
2010    /// ReceiptRational => Some(T::Rational)
2011    /// ReceiptTime     => Some(T::Time)
2012    /// ```
2013    ///
2014    /// Type projection:
2015    /// - `T` defines schema
2016    /// - storage via [`VirtualDynField`]
2017    ///
2018    /// ## Context
2019    ///
2020    /// [`ShareBalanceContext`] supplies:
2021    /// - bounds ([`VirtualDynBound`])
2022    /// - empty extensions ([`empty_virtual_extension!`](frame_suite::empty_virtual_extension))
2023    ///
2024    /// Structure is abstract; layout comes from context.
2025    mod receipt {
2026        use super::*;
2027
2028        /// Returns the original deposit value of the receipt.
2029        ///
2030        /// [`LazyBalance::Receipt`] virtual field: `principal`
2031        ///
2032        /// Internally resolved from the discriminant [`ReceiptAsset`] field at index `0`.
2033        pub fn principal<T: LazyBalance>(receipt: &T::Receipt) -> Option<T::Asset> {
2034            <T::Receipt as DynFieldHelpers<ReceiptAsset>>::index_get(receipt, 0)
2035        }
2036
2037        /// Sets the original deposit value of the receipt.
2038        ///
2039        /// [`LazyBalance::Receipt`] virtual field: `principal`
2040        ///
2041        /// Writes to the discriminant [`ReceiptAsset`] field at index `0`.
2042        pub fn set_principal<T: LazyBalance>(
2043            receipt: &mut T::Receipt,
2044            value: T::Asset,
2045        ) -> Result<(), ShareBalanceError> {
2046            <T::Receipt as DynFieldHelpers<ReceiptAsset>>::index_set(receipt, 0, value)
2047                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
2048        }
2049
2050        /// Returns the total shares provided for the receipt.
2051        ///
2052        /// [`LazyBalance::Receipt`] virtual field: `shares`
2053        ///
2054        /// Internally resolved from the discriminant [`ReceiptAsset`] field at index `1`.
2055        pub fn shares<T: LazyBalance>(receipt: &T::Receipt) -> Option<T::Asset> {
2056            <T::Receipt as DynFieldHelpers<ReceiptAsset>>::index_get(receipt, 1)
2057        }
2058
2059        /// Sets the total shares provided for the receipt.
2060        ///
2061        /// [`LazyBalance::Receipt`] virtual field: `shares`
2062        ///
2063        /// Writes to the discriminant [`ReceiptAsset`] field at index `1`.
2064        pub fn set_shares<T: LazyBalance>(
2065            receipt: &mut T::Receipt,
2066            value: T::Asset,
2067        ) -> Result<(), ShareBalanceError> {
2068            <T::Receipt as DynFieldHelpers<ReceiptAsset>>::index_set(receipt, 1, value)
2069                .map_err(|_| ShareBalanceError::CorruptedVirtualField)
2070        }
2071
2072        /// Returns the scaling factor (share-price) associated with the receipt
2073        /// at the time of deposit.
2074        ///
2075        /// [`LazyBalance::Receipt`] virtual field: `bias`
2076        ///
2077        /// Internally resolved from the discriminant [`ReceiptRational`] field.
2078        pub fn bias<T: LazyBalance>(receipt: &T::Receipt) -> Option<T::Rational> {
2079            <T::Receipt as DynFieldHelpers<ReceiptRational>>::get(receipt)
2080        }
2081
2082        /// Sets the scaling factor (share-price) associated with the receipt
2083        /// at the time of deposit.
2084        ///
2085        /// [`LazyBalance::Receipt`] virtual field: `bias`
2086        ///
2087        /// Writes to the discriminant [`ReceiptRational`] field.
2088        pub fn set_bias<T: LazyBalance>(receipt: &mut T::Receipt, value: T::Rational) {
2089            <T::Receipt as DynFieldHelpers<ReceiptRational>>::set(receipt, value)
2090        }
2091
2092        /// Returns the checkpoint time of the receipt.
2093        ///
2094        /// [`LazyBalance::Receipt`] virtual field: `checkpoint`
2095        ///
2096        /// Internally resolved from the discriminant [`ReceiptTime`] field.
2097        pub fn checkpoint<T: LazyBalance>(receipt: &T::Receipt) -> Option<T::Time> {
2098            <T::Receipt as DynFieldHelpers<ReceiptTime>>::get(receipt)
2099        }
2100
2101        /// Sets the checkpoint time of the receipt.
2102        ///
2103        /// [`LazyBalance::Receipt`] virtual field: `checkpoint`
2104        ///
2105        /// Writes to the discriminant [`ReceiptTime`] field.
2106        pub fn set_checkpoint<T: LazyBalance>(receipt: &mut T::Receipt, value: T::Time) {
2107            <T::Receipt as DynFieldHelpers<ReceiptTime>>::set(receipt, value)
2108        }
2109
2110        /// [`LazyBalance::Receipt`] virtual field layout for the
2111        /// [`ReceiptAsset`] discriminant
2112        ///
2113        /// Allocates two asset fields:
2114        /// - `principal`  (internally index `0`)
2115        /// - `shares`  (internally index `1`)
2116        impl<T> VirtualDynBound<ReceiptAsset> for ShareBalanceContext<T> {
2117            type Bound = ConstU32<2>;
2118        }
2119
2120        /// [`LazyBalance::Receipt`] virtual field layout for the
2121        /// [`ReceiptRational`] discriminant
2122        ///
2123        /// Allocates one rational field:
2124        /// - `bias`
2125        impl<T> VirtualDynBound<ReceiptRational> for ShareBalanceContext<T> {
2126            type Bound = ConstU32<1>;
2127        }
2128
2129        /// [`LazyBalance::Receipt`] virtual field layout for the
2130        /// [`ReceiptTime`] discriminant
2131        ///
2132        /// Allocates one time field:
2133        /// - `checkpoint`
2134        impl<T> VirtualDynBound<ReceiptTime> for ShareBalanceContext<T> {
2135            type Bound = ConstU32<1>;
2136        }
2137
2138        // `LazyBalance::Receipt` [`virtual`](frame_suite::virtuals) extension schema for the
2139        // `ReceiptAddon` discriminant
2140        //
2141        // Defines an empty extension schema.
2142        //
2143        // Receipts do not support addon-backed fields, and always behave
2144        // as having no extension data.
2145        empty_virtual_extension!(
2146            target: T::Receipt,
2147            tag: ReceiptAddon,
2148            schema: ShareBalanceContext<T>,
2149            generics: [T]
2150        );
2151    }
2152
2153    // ===============================================================================
2154    // ```````````````````````````` SHARE-BALANCE ERRORS `````````````````````````````
2155    // ===============================================================================
2156
2157    /// Errors that can occur during [`ShareBalanceFamily`]
2158    /// plugin operations.
2159    ///
2160    /// Covers:
2161    /// - validation failures (invalid inputs, receipts)
2162    /// - state violations (uninitialized, drained, inconsistent)
2163    /// - arithmetic issues (overflow, underflow, precision)
2164    #[derive(
2165        Clone,
2166        Copy,
2167        PartialEq,
2168        Eq,
2169        Debug,
2170        Encode,
2171        Decode,
2172        MaxEncodedLen,
2173        DecodeWithMemTracking,
2174        TypeInfo,
2175    )]
2176    pub enum ShareBalanceError {
2177        /// Internal inconsistency in virtual field storage (corrupted or missing data).
2178        CorruptedVirtualField,
2179
2180        /// Invalid input parameters passed to the plugin (tag/discriminant mismatch or malformed input).
2181        InvalidPluginParams,
2182
2183        /// Deposit amount must be non-zero.
2184        ZeroDepositNotAllowed,
2185
2186        /// Operation requires an initialized balance (must be created via deposit first).
2187        BalanceNotInitiatedViaDeposit,
2188
2189        /// Cannot deposit into a fully drained balance (`bias == 0`); requires mint to recover.
2190        BalanceDrainedCannotDeposit,
2191
2192        /// Fixed-point arithmetic failed due to insufficient precision or invalid scaling.
2193        InadequatePrecision,
2194
2195        /// Deposit too small relative to balance, resulting in floored zero shares after conversion.
2196        LessThanOneShareDerived,
2197
2198        /// Overflow occurred while updating total effective asset value.
2199        AssetOverflow,
2200
2201        /// Underflow occurred while reducing effective asset value.
2202        AssetUnderflow,
2203
2204        /// Overflow occurred while updating total issued shares.
2205        SharesOverflow,
2206
2207        /// Underflow occurred while reducing issued shares.
2208        SharesUnderflow,
2209
2210        /// Operation requires existing deposits (issued shares must be non-zero).
2211        RequiresExistingDeposits,
2212
2213        /// Adjustment (mint/reap) amount must be non-zero.
2214        ZeroAdjustmentNotAllowed,
2215
2216        /// Receipt is invalid (missing fields, malformed, or inconsistent with balance).
2217        InvalidReceipt,
2218
2219        /// Failure during fixed-point scaling conversion (e.g., division by scaling factor).
2220        FixedPointScalingFailed,
2221
2222        /// All deposits have already been withdrawn (no remaining shares).
2223        AllDepositsWithdrawn,
2224    }
2225
2226    impl Into<DispatchError> for ShareBalanceError {
2227        fn into(self) -> DispatchError {
2228            match self {
2229                ShareBalanceError::CorruptedVirtualField => {
2230                    DispatchError::Other("CorruptedVirtualField")
2231                }
2232                ShareBalanceError::InvalidPluginParams => {
2233                    DispatchError::Other("InvalidPluginParams")
2234                }
2235                ShareBalanceError::InadequatePrecision => {
2236                    DispatchError::Other("InadequatePrecision")
2237                }
2238                ShareBalanceError::AssetOverflow => DispatchError::Other("AssetOverflow"),
2239                ShareBalanceError::SharesOverflow => DispatchError::Other("SharesOverflow"),
2240                ShareBalanceError::FixedPointScalingFailed => {
2241                    DispatchError::Other("FixedPointScalingFailed")
2242                }
2243                ShareBalanceError::AssetUnderflow => DispatchError::Other("AssetUnderflow"),
2244                ShareBalanceError::InvalidReceipt => DispatchError::Other("InvalidReceipt"),
2245                ShareBalanceError::SharesUnderflow => DispatchError::Other("SharesUnderflow"),
2246                ShareBalanceError::BalanceNotInitiatedViaDeposit => {
2247                    DispatchError::Other("BalanceNotInitiatedViaDeposit")
2248                }
2249                ShareBalanceError::BalanceDrainedCannotDeposit => {
2250                    DispatchError::Other("BalanceDrainedCannotDeposit")
2251                }
2252                ShareBalanceError::ZeroDepositNotAllowed => {
2253                    DispatchError::Other("ZeroDepositNotAllowed")
2254                }
2255                ShareBalanceError::AllDepositsWithdrawn => {
2256                    DispatchError::Other("AllDepositsWithdrawn")
2257                }
2258                ShareBalanceError::RequiresExistingDeposits => {
2259                    DispatchError::Other("RequiresExistingDeposits")
2260                }
2261                ShareBalanceError::LessThanOneShareDerived => {
2262                    DispatchError::Other("LessThanOneShareDerived")
2263                }
2264                ShareBalanceError::ZeroAdjustmentNotAllowed => DispatchError::Other("ZeroAdjustmentNotAllowed"),
2265            }
2266        }
2267    }
2268
2269    /// Provides the concrete error type for the [`LazyBalance`] system
2270    ///
2271    /// This binds the [`LazyBalanceError`] discriminant to [`ShareBalanceError`],
2272    /// allowing all LazyBalance plugin models with context [`ShareBalanceContext`]
2273    /// to resolve their error type.
2274    impl<T> VirtualError<LazyBalanceError> for ShareBalanceContext<T> {
2275        type Error = ShareBalanceError;
2276    }
2277
2278    // ===============================================================================
2279    // `````````````````````` SHARE-BALANCE MODEL-CHECKER TESTS ``````````````````````
2280    // ===============================================================================
2281
2282    #[cfg(test)]
2283    mod model_checker {
2284
2285        // ===============================================================================
2286        // ``````````````````````````````````` IMPORTS ```````````````````````````````````
2287        // ===============================================================================
2288
2289        // --- Local module imports ---
2290        use super::{mock::*, *};
2291
2292        // --- Scale-codec crates ---
2293        use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
2294        use scale_info::TypeInfo;
2295
2296        // --- Substrate primitives ---
2297        use sp_runtime::{
2298            traits::{CheckedAdd, CheckedDiv, CheckedSub, One, Saturating, Zero},
2299            FixedPointNumber, FixedU128,
2300        };
2301
2302        // --- Standard library ---
2303        use std::{
2304            collections::BTreeMap,
2305            env,
2306            fmt::Debug,
2307            hash::{DefaultHasher, Hash, Hasher},
2308            marker::PhantomData,
2309            path::{Path, PathBuf},
2310            u128,
2311        };
2312
2313        // ===============================================================================
2314        // `````````````````````````````````` CONSTANTS ``````````````````````````````````
2315        // ===============================================================================
2316
2317        // --- Test identities ---
2318
2319        /// Primary test user (baseline subject).
2320        const ALICE: UserID = UserID(0u32);
2321
2322        /// Secondary test user (used in multi-user scenarios).
2323        #[allow(unused)]
2324        const BOB: UserID = UserID(1u32);
2325
2326        /// Additional test user for extended scenarios.
2327        #[allow(unused)]
2328        const CHARLIE: UserID = UserID(2u32);
2329
2330        /// Additional test user for extended scenarios.
2331        #[allow(unused)]
2332        const DAVE: UserID = UserID(3u32);
2333
2334        /// Additional test user for extended scenarios.
2335        #[allow(unused)]
2336        const EVE: UserID = UserID(3u32);
2337
2338        /// Default set of users used in most tests.
2339        const USERS: &[UserID] = &[ALICE, BOB];
2340
2341        // --- Deposit test values ---
2342
2343        /// Comprehensive deposit values covering edge cases, powers, primes, and stress inputs.
2344        const STRESS_DEPOSITS: &[u128] = &[
2345            // Identity / base
2346            0,
2347            1,
2348            2,
2349            3,
2350            // Powers of two
2351            4,
2352            8,
2353            16,
2354            32,
2355            64,
2356            256,
2357            1024,
2358            65536,
2359            1_048_576,
2360            // Boundaries (2^n +/- 1)
2361            7,
2362            15,
2363            31,
2364            63,
2365            127,
2366            1023,
2367            1025,
2368            65535,
2369            65537,
2370            1_048_575,
2371            1_048_577,
2372            // Primes (spread out)
2373            11,
2374            73,
2375            101,
2376            509,
2377            997,
2378            5003,
2379            99_991,
2380            123_457,
2381            999_983,
2382            1_000_003,
2383            // "Ugly" composites
2384            6,
2385            12,
2386            60,
2387            120,
2388            360,
2389            840,
2390            2520,
2391            5040,
2392            // Patterned numbers
2393            111,
2394            333,
2395            777,
2396            999,
2397            // Large awkward values
2398            16_777_213,
2399            2_147_483_647,
2400            // Ratio extremes
2401            10_000_000,
2402        ];
2403
2404        /// Adjustment values used to test proportional changes and edge conditions.
2405        const STRESS_ADJUSTMENTS: &[u128] = &[
2406            // Identity
2407            0, 1, 2, 3, // Powers of two
2408            4, 8, 16, 32, 64, 128, 256, // Boundaries
2409            7, 15, 31, 63, 255, 257, 1023, 1025, 2047, 4095, 8191, // Primes
2410            5, 77, 101, 333, 777, 999, 4093, 8191, 16381, 32749, // Dense composites
2411            6, 12, 24, 60, 120, 360, // High ratio stress
2412            10_000, 100_000,
2413        ];
2414
2415        // --- Safe test ranges ---
2416
2417        /// Conservative deposit values that avoid overflow and extreme ratios.
2418        const PRACTICAL_DEPOSITS: &[u128] = &[500, 750, 1000, 1250, 1500, 1750, 2000, 2500, 3000];
2419
2420        /// Conservative adjustment values for stable, low-risk test scenarios.
2421        const PRACTICAL_ADJUSTMENTS: &[u128] = &[50, 75, 100, 150, 200];
2422
2423        // --- Tolerances ---
2424
2425        /// Maximum allowed basis points deviation (withdraw drifts between lazy
2426        /// and manual balance model) for stress tests.
2427        const STRESS_BPS: u32 = 20;
2428
2429        /// Maximum allowed absolute difference (withdraw drifts between lazy
2430        /// and manual balance model) for stress tests.
2431        const STRESS_DIFF: u32 = 5;
2432
2433        /// Maximum allowed basis points deviation (withdraw drifts between lazy
2434        /// and manual balance model) for practical tests.
2435        const PRACTICAL_BPS: u32 = 10;
2436
2437        /// Maximum allowed absolute difference (withdraw drifts between lazy
2438        /// and manual balance model) for practial tests.
2439        const PRACTICAL_DIFF: u32 = 2;
2440
2441        // --- Limits ---
2442
2443        /// Maximum balance-operations sequence depth for model-check scenarios.
2444        const MAX_DEPTH: u32 = 9;
2445
2446        // --- Subjects ---
2447
2448        /// Collection of test subjects which is empty by default, since
2449        /// [`ShareBalanceFamily`] provides unbounded limits.
2450        const SUBJECTS: &[TestSubject] = &[];
2451
2452        /// Resolves a the model-checker results directory path relative to the current source file.
2453        ///
2454        /// This function walks up from the current working directory until it finds
2455        /// the source file corresponding to `file!()`. Once found, it returns a path
2456        /// by joining the source file's directory with the provided `name`.
2457        ///
2458        /// - `name`: The name of the results directory to append.
2459        ///
2460        /// ## Example
2461        /// ```ignore
2462        /// let path = results_dir("outputs");
2463        /// ```
2464        fn results_dir(name: &str) -> PathBuf {
2465            let rel_file = Path::new(file!());
2466            let mut base = env::current_dir().unwrap();
2467
2468            let src_file = loop {
2469                let candidate = base.join(rel_file);
2470                if candidate.exists() {
2471                    break candidate;
2472                }
2473                if !base.pop() {
2474                    panic!("Could not resolve source file path");
2475                }
2476            };
2477
2478            let src_dir = src_file.parent().unwrap();
2479            src_dir.join(name)
2480        }
2481
2482        // ===============================================================================
2483        // ````````````````````````````````` MODEL-CHECKS ````````````````````````````````
2484        // ===============================================================================
2485
2486        #[test]
2487        #[ignore]
2488        fn model_practical_check() {
2489            let mut results = Tester::initiate_results();
2490
2491            Tester::explore(
2492                USERS,
2493                PRACTICAL_DEPOSITS,
2494                PRACTICAL_ADJUSTMENTS,
2495                SUBJECTS,
2496                MAX_DEPTH,
2497                PRACTICAL_BPS,
2498                PRACTICAL_DIFF,
2499                &mut results,
2500            );
2501
2502            Tester::write_reports(results_dir("model_check"), &results, false, false, false);
2503        }
2504
2505        #[test]
2506        #[ignore]
2507        fn model_stress_check() {
2508            let mut results = Tester::initiate_results();
2509
2510            Tester::explore(
2511                USERS,
2512                STRESS_DEPOSITS,
2513                STRESS_ADJUSTMENTS,
2514                SUBJECTS,
2515                MAX_DEPTH,
2516                STRESS_BPS,
2517                STRESS_DIFF,
2518                &mut results,
2519            );
2520
2521            Tester::write_reports(results_dir("stress_check"), &results, false, true, false);
2522        }
2523
2524        // ===============================================================================
2525        // ```````````````````````````` TRAP-CHECKS (DEPOSIT) ````````````````````````````
2526        // ===============================================================================
2527
2528        #[test]
2529        #[ignore]
2530        fn trap_empty_deposit() {
2531            let mut results = Tester::initiate_results();
2532
2533            let traps: TrapConfig = BalanceTraps {
2534                trap: |_, op| match op {
2535                    BalanceOp::Deposit(_, v, _) => {
2536                        let empty_deposit = v.is_zero();
2537                        empty_deposit
2538                    }
2539                    _ => false,
2540                },
2541
2542                flow: |_, _| true,
2543
2544                reason: format!("{:?}", ShareBalanceError::ZeroDepositNotAllowed),
2545            };
2546
2547            Tester::explore_traps(
2548                &USERS,
2549                STRESS_DEPOSITS,
2550                STRESS_ADJUSTMENTS,
2551                SUBJECTS,
2552                MAX_DEPTH,
2553                STRESS_BPS,
2554                STRESS_DIFF,
2555                Some(traps),
2556                &mut results,
2557            );
2558
2559            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2560        }
2561
2562        #[test]
2563        #[ignore]
2564        fn trap_deposit_after_drain() {
2565            let mut results = Tester::initiate_results();
2566
2567            let traps: TrapConfig = BalanceTraps {
2568                trap: |state, op| match op {
2569                    BalanceOp::Deposit(_, v, _) => {
2570                        let balance = &state.lazy.balance;
2571                        let effective = balance::effective::<MockShareBalance>(balance);
2572                        let bias = balance::bias::<MockShareBalance>(balance);
2573
2574                        let fresh_balance = effective.is_none() && bias.is_none();
2575
2576                        if fresh_balance {
2577                            return false;
2578                        }
2579
2580                        let empty_deposit = v.is_zero();
2581
2582                        let drained = effective.unwrap().is_zero() && bias.unwrap().is_zero();
2583
2584                        !empty_deposit && drained
2585                    }
2586                    _ => false,
2587                },
2588
2589                flow: |_, _| true,
2590
2591                reason: format!("{:?}", ShareBalanceError::BalanceDrainedCannotDeposit),
2592            };
2593
2594            Tester::explore_traps(
2595                &USERS,
2596                STRESS_DEPOSITS,
2597                STRESS_ADJUSTMENTS,
2598                SUBJECTS,
2599                MAX_DEPTH,
2600                STRESS_BPS,
2601                STRESS_DIFF,
2602                Some(traps),
2603                &mut results,
2604            );
2605
2606            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2607        }
2608
2609        #[test]
2610        #[ignore]
2611        fn trap_almost_zero_share_deposit() {
2612            let mut results = Tester::initiate_results();
2613
2614            let traps: TrapConfig = BalanceTraps {
2615                trap: |state, op| match op {
2616                    BalanceOp::Deposit(user, v, _) => {
2617                        let balance = &state.lazy.balance;
2618                        let effective = balance::effective::<MockShareBalance>(balance);
2619                        let bias = balance::bias::<MockShareBalance>(balance);
2620                        let issued = balance::issued::<MockShareBalance>(balance);
2621
2622                        let fresh_balance = effective.is_none() && bias.is_none();
2623
2624                        if fresh_balance {
2625                            return false;
2626                        }
2627
2628                        let empty_deposit = v.is_zero();
2629
2630                        let drained = effective.unwrap().is_zero() && bias.unwrap().is_zero();
2631
2632                        let zero_share = {
2633                            match issued.unwrap().is_zero() {
2634                                true => false,
2635                                false => {
2636                                    let adjusted = issued.unwrap().saturating_sub(One::one());
2637                                    match effective.unwrap().checked_add(adjusted) {
2638                                        Some(total) => {
2639                                            let min_required = total / issued.unwrap();
2640                                            *v < min_required
2641                                        }
2642                                        None => false,
2643                                    }
2644                                }
2645                            }
2646                        };
2647
2648                        let duplicate = state.receipts.contains_key(&user);
2649
2650                        !duplicate && !drained && !empty_deposit && zero_share
2651                    }
2652                    _ => false,
2653                },
2654
2655                flow: |_, _| true,
2656
2657                reason: format!("{:?}", ShareBalanceError::LessThanOneShareDerived),
2658            };
2659
2660            Tester::explore_traps(
2661                &USERS,
2662                STRESS_DEPOSITS,
2663                STRESS_ADJUSTMENTS,
2664                SUBJECTS,
2665                MAX_DEPTH,
2666                STRESS_BPS,
2667                STRESS_DIFF,
2668                Some(traps),
2669                &mut results,
2670            );
2671
2672            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2673        }
2674
2675        #[test]
2676        #[ignore]
2677        fn trap_deposit_precision() {
2678            for _ in STRESS_DEPOSITS {
2679                let mut results = Tester::initiate_results();
2680
2681                let traps: TrapConfig = BalanceTraps {
2682                    trap: |state, op| match op {
2683                        BalanceOp::Deposit(user, v, _) => {
2684                            let balance = &state.lazy.balance;
2685                            let effective = balance::effective::<MockShareBalance>(balance);
2686                            let bias = balance::bias::<MockShareBalance>(balance);
2687                            let issued = balance::issued::<MockShareBalance>(balance);
2688
2689                            let fresh_balance =
2690                                effective.is_none() && bias.is_none() && issued.is_none();
2691
2692                            if fresh_balance {
2693                                return false;
2694                            }
2695
2696                            let empty_deposit = v.is_zero();
2697
2698                            let drained = effective.unwrap().is_zero() && bias.unwrap().is_zero();
2699
2700                            let zero_share = {
2701                                match issued.unwrap().is_zero() {
2702                                    true => false,
2703                                    false => {
2704                                        let adjusted = issued.unwrap().saturating_sub(One::one());
2705                                        match effective.unwrap().checked_add(adjusted) {
2706                                            Some(total) => {
2707                                                let min_required = total / issued.unwrap();
2708                                                *v < min_required
2709                                            }
2710                                            None => false,
2711                                        }
2712                                    }
2713                                }
2714                            };
2715
2716                            let duplicate = state.receipts.contains_key(&user);
2717
2718                            let derive_fail = FixedU128::saturating_from_integer(*v)
2719                                .checked_div(&bias.unwrap())
2720                                .is_none();
2721
2722                            !duplicate && !drained && !empty_deposit && !zero_share && derive_fail
2723                        }
2724                        _ => false,
2725                    },
2726
2727                    flow: |_, _| true,
2728
2729                    reason: format!("{:?}", ShareBalanceError::InadequatePrecision),
2730                };
2731
2732                Tester::explore_traps(
2733                    &USERS,
2734                    STRESS_DEPOSITS,
2735                    STRESS_ADJUSTMENTS,
2736                    SUBJECTS,
2737                    (MAX_DEPTH + 3).min(11),
2738                    STRESS_BPS,
2739                    STRESS_DIFF,
2740                    Some(traps),
2741                    &mut results,
2742                );
2743
2744                Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2745
2746                if !results.trap.is_empty() {
2747                    break;
2748                }
2749            }
2750        }
2751
2752        #[test]
2753        #[ignore]
2754        fn trap_manual_balance_duplicate_deposit() {
2755            let mut results = Tester::initiate_results();
2756
2757            let traps: TrapConfig = BalanceTraps {
2758                trap: |state, op| match op {
2759                    BalanceOp::Deposit(user, v, _) => {
2760                        let balance = &state.lazy.balance;
2761                        let effective = balance::effective::<MockShareBalance>(balance);
2762                        let bias = balance::bias::<MockShareBalance>(balance);
2763                        let issued = balance::issued::<MockShareBalance>(balance);
2764
2765                        let fresh_balance = effective.is_none() && bias.is_none();
2766
2767                        if fresh_balance {
2768                            return false;
2769                        }
2770
2771                        let empty_deposit = v.is_zero();
2772
2773                        let drained = effective.unwrap().is_zero() && bias.unwrap().is_zero();
2774
2775                        let zero_share = {
2776                            match issued.unwrap().is_zero() {
2777                                true => false,
2778                                false => {
2779                                    let adjusted = issued.unwrap().saturating_sub(One::one());
2780                                    match effective.unwrap().checked_add(adjusted) {
2781                                        Some(total) => {
2782                                            let min_required = total / issued.unwrap();
2783                                            *v < min_required
2784                                        }
2785                                        None => false,
2786                                    }
2787                                }
2788                            }
2789                        };
2790
2791                        let duplicate = state.receipts.contains_key(&user);
2792
2793                        let derive_fail = FixedU128::saturating_from_integer(*v)
2794                            .checked_div(&bias.unwrap())
2795                            .is_none();
2796
2797                        !drained && !empty_deposit && !zero_share && !derive_fail && duplicate
2798                    }
2799                    _ => false,
2800                },
2801
2802                flow: |_, _| true,
2803
2804                reason: format!("{:?}", ManualError::DuplicateDeposit),
2805            };
2806
2807            Tester::explore_traps(
2808                &USERS,
2809                STRESS_DEPOSITS,
2810                STRESS_ADJUSTMENTS,
2811                SUBJECTS,
2812                MAX_DEPTH,
2813                STRESS_BPS,
2814                STRESS_DIFF,
2815                Some(traps),
2816                &mut results,
2817            );
2818
2819            if results.trap.is_empty() {
2820                panic!("None Trapped");
2821            }
2822
2823            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2824        }
2825
2826        // ===============================================================================
2827        // ```````````````````````````` TRAP-CHECKS (WITHDRAW) ```````````````````````````
2828        // ===============================================================================
2829
2830        #[test]
2831        #[ignore]
2832        fn trap_unknown_withdrawal_for_manual() {
2833            let mut results = Tester::initiate_results();
2834
2835            let traps: TrapConfig = BalanceTraps {
2836                trap: |state, op| match op {
2837                    BalanceOp::Withdraw(user) => {
2838                        let exists = state.receipts.contains_key(&user);
2839                        !exists
2840                    }
2841                    _ => false,
2842                },
2843
2844                flow: |_, _| true,
2845
2846                reason: "ModelChecker::WithdrawReceiptMissing".to_string(),
2847            };
2848
2849            Tester::explore_traps(
2850                &USERS,
2851                STRESS_DEPOSITS,
2852                STRESS_ADJUSTMENTS,
2853                SUBJECTS,
2854                MAX_DEPTH,
2855                STRESS_BPS,
2856                STRESS_DIFF,
2857                Some(traps),
2858                &mut results,
2859            );
2860
2861            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2862        }
2863
2864        // ===============================================================================
2865        // `````````````````````````````` TRAP-CHECKS (MINT) `````````````````````````````
2866        // ===============================================================================
2867
2868        #[test]
2869        #[ignore]
2870        fn trap_mint_fresh_balance() {
2871            let mut results = Tester::initiate_results();
2872
2873            let traps: TrapConfig = BalanceTraps {
2874                trap: |state, op| match op {
2875                    BalanceOp::Mint(v, _) => {
2876                        let balance = &state.lazy.balance;
2877                        let effective = balance::effective::<MockShareBalance>(balance);
2878                        let bias = balance::bias::<MockShareBalance>(balance);
2879
2880                        let fresh_balance = effective.is_none() && bias.is_none();
2881
2882                        let zero_mint = v.is_zero();
2883
2884                        fresh_balance && !zero_mint
2885                    }
2886                    _ => false,
2887                },
2888
2889                flow: |_, _| true,
2890
2891                reason: format!("{:?}", ShareBalanceError::BalanceNotInitiatedViaDeposit),
2892            };
2893
2894            Tester::explore_traps(
2895                &USERS,
2896                STRESS_DEPOSITS,
2897                STRESS_ADJUSTMENTS,
2898                SUBJECTS,
2899                MAX_DEPTH,
2900                STRESS_BPS,
2901                STRESS_DIFF,
2902                Some(traps),
2903                &mut results,
2904            );
2905
2906            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2907        }
2908
2909        #[test]
2910        #[ignore]
2911        fn trap_manual_fresh_balance_zero_mint() {
2912            let mut results = Tester::initiate_results();
2913
2914            let traps: TrapConfig = BalanceTraps {
2915                trap: |state, op| match op {
2916                    BalanceOp::Mint(v, _) => {
2917                        let balance = &state.lazy.balance;
2918                        let effective = balance::effective::<MockShareBalance>(balance);
2919                        let bias = balance::bias::<MockShareBalance>(balance);
2920
2921                        let fresh_balance = effective.is_none() && bias.is_none();
2922
2923                        let zero_mint = v.is_zero();
2924
2925                        fresh_balance && zero_mint
2926                    }
2927                    _ => false,
2928                },
2929
2930                flow: |state, op| match op {
2931                    BalanceOp::Mint(..) => {
2932                        if state.trace.len().is_zero() {
2933                            true
2934                        } else {
2935                            false
2936                        }
2937                    }
2938                    _ => false,
2939                },
2940
2941                reason: format!("{:?}", ManualError::MintWithoutDeposits),
2942            };
2943
2944            Tester::explore_traps(
2945                &USERS,
2946                STRESS_DEPOSITS,
2947                &[0],
2948                SUBJECTS,
2949                MAX_DEPTH,
2950                STRESS_BPS,
2951                STRESS_DIFF,
2952                Some(traps),
2953                &mut results,
2954            );
2955
2956            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
2957        }
2958
2959        #[test]
2960        #[ignore]
2961        fn trap_mint_without_deposits() {
2962            let mut results = Tester::initiate_results();
2963
2964            let traps: TrapConfig = BalanceTraps {
2965                trap: |state, op| match op {
2966                    BalanceOp::Mint(v, _) => {
2967                        let no_receipts = state.receipts.is_empty();
2968
2969                        let balance = &state.lazy.balance;
2970                        let effective = balance::effective::<MockShareBalance>(balance);
2971                        let bias = balance::bias::<MockShareBalance>(balance);
2972
2973                        let fresh_balance = effective.is_none() && bias.is_none();
2974
2975                        // lazy balance accepts zero mint but manual doesn't
2976                        let zero_mint = v.is_zero();
2977
2978                        no_receipts && !fresh_balance && !zero_mint
2979                    }
2980                    _ => false,
2981                },
2982
2983                flow: |_, _| true,
2984
2985                reason: format!("{:?}", ShareBalanceError::RequiresExistingDeposits),
2986            };
2987
2988            Tester::explore_traps(
2989                &USERS,
2990                STRESS_DEPOSITS,
2991                STRESS_ADJUSTMENTS,
2992                SUBJECTS,
2993                MAX_DEPTH,
2994                STRESS_BPS,
2995                STRESS_DIFF,
2996                Some(traps),
2997                &mut results,
2998            );
2999
3000            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3001        }
3002
3003        #[test]
3004        #[ignore]
3005        fn trap_manual_balance_without_deposit_zero_mint() {
3006            let mut results = Tester::initiate_results();
3007
3008            let traps: TrapConfig = BalanceTraps {
3009                trap: |state, op| match op {
3010                    BalanceOp::Mint(v, _) => {
3011                        let no_receipts = state.receipts.is_empty();
3012
3013                        let balance = &state.lazy.balance;
3014                        let effective = balance::effective::<MockShareBalance>(balance);
3015                        let bias = balance::bias::<MockShareBalance>(balance);
3016
3017                        let fresh_balance = effective.is_none() && bias.is_none();
3018
3019                        let zero_mint = v.is_zero();
3020
3021                        no_receipts && !fresh_balance && zero_mint
3022                    }
3023                    _ => false,
3024                },
3025
3026                flow: |_, _| true,
3027
3028                reason: format!("{:?}", ManualError::MintWithoutDeposits),
3029            };
3030
3031            Tester::explore_traps(
3032                &USERS,
3033                STRESS_DEPOSITS,
3034                STRESS_ADJUSTMENTS,
3035                SUBJECTS,
3036                MAX_DEPTH,
3037                STRESS_BPS,
3038                STRESS_DIFF,
3039                Some(traps),
3040                &mut results,
3041            );
3042
3043            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3044        }
3045
3046        #[test]
3047        #[ignore]
3048        fn trap_zero_mint_fresh_balance_manual_trap() {
3049            let mut results = Tester::initiate_results();
3050
3051            let traps: TrapConfig = BalanceTraps {
3052                trap: |state, op| match op {
3053                    BalanceOp::Mint(v, _) => {
3054                        let balance = &state.lazy.balance;
3055                        let effective = balance::effective::<MockShareBalance>(balance);
3056                        let bias = balance::bias::<MockShareBalance>(balance);
3057
3058                        let fresh_balance = effective.is_none() && bias.is_none();
3059
3060                        let zero_mint = v.is_zero();
3061
3062                        fresh_balance && zero_mint
3063                    }
3064                    _ => false,
3065                },
3066
3067                flow: |state, op| match op {
3068                    BalanceOp::Mint(..) => {
3069                        if state.trace.len().is_zero() {
3070                            true
3071                        } else {
3072                            false
3073                        }
3074                    }
3075                    _ => false,
3076                },
3077
3078                reason: format!("{:?}", ManualError::MintWithoutDeposits),
3079            };
3080
3081            Tester::explore_traps(
3082                &USERS,
3083                STRESS_DEPOSITS,
3084                &[0],
3085                SUBJECTS,
3086                MAX_DEPTH,
3087                STRESS_BPS,
3088                STRESS_DIFF,
3089                Some(traps),
3090                &mut results,
3091            );
3092
3093            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3094        }
3095
3096        #[test]
3097        #[ignore]
3098        fn trap_big_width_mint() {
3099            let mut results = Tester::initiate_results();
3100
3101            let traps: TrapConfig = BalanceTraps {
3102                trap: |state, op| match op {
3103                    BalanceOp::Mint(v, _) => {
3104                        let balance = &state.lazy.balance;
3105                        let effective = balance::effective::<MockShareBalance>(balance);
3106                        let bias = balance::bias::<MockShareBalance>(balance);
3107
3108                        if effective.is_none() && bias.is_none() {
3109                            return false;
3110                        }
3111
3112                        let overflow = v.checked_add(&effective.unwrap()).is_none();
3113
3114                        let no_deposits = state.receipts.is_empty();
3115
3116                        let zero_manual = state.manual.total_fixed().is_zero();
3117
3118                        let manual_no_users = state.manual.users.is_empty();
3119
3120                        let manual_drain_shares = state.manual.before_drain.is_some();
3121
3122                        let manual_collapse =
3123                            !manual_no_users && zero_manual && !manual_drain_shares;
3124
3125                        !overflow && !no_deposits && !manual_collapse
3126                    }
3127                    _ => false,
3128                },
3129
3130                flow: |_, _| true,
3131
3132                reason: format!("{:?}", ShareBalanceError::InadequatePrecision),
3133            };
3134
3135            let adjustments = {
3136                let mut v = STRESS_ADJUSTMENTS.to_vec();
3137                v.push(u128::MAX);
3138                v
3139            };
3140
3141            Tester::explore_traps(
3142                &USERS,
3143                STRESS_DEPOSITS,
3144                &adjustments,
3145                SUBJECTS,
3146                MAX_DEPTH,
3147                STRESS_BPS,
3148                STRESS_DIFF,
3149                Some(traps),
3150                &mut results,
3151            );
3152
3153            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3154        }
3155
3156        #[test]
3157        #[ignore]
3158        fn trap_mint_overflow() {
3159            let mut results = Tester::initiate_results();
3160
3161            let traps: TrapConfig = BalanceTraps {
3162                trap: |state, op| match op {
3163                    BalanceOp::Mint(v, _) => {
3164                        let balance = &state.lazy.balance;
3165                        let effective = balance::effective::<MockShareBalance>(balance);
3166                        let bias = balance::bias::<MockShareBalance>(balance);
3167
3168                        if effective.is_none() && bias.is_none() {
3169                            return false;
3170                        }
3171
3172                        let overflow = v.checked_add(&effective.unwrap()).is_none();
3173
3174                        let no_deposits = state.receipts.is_empty();
3175
3176                        overflow && !no_deposits
3177                    }
3178                    _ => false,
3179                },
3180
3181                flow: |state, op| match op {
3182                    BalanceOp::Mint(v, _) => {
3183                        let mut drained = false;
3184                        let mut reaped_till = u128::zero();
3185                        for op in state.trace.iter().rev() {
3186                            match op {
3187                                BalanceOp::Drain => {
3188                                    drained = true;
3189                                    break;
3190                                }
3191                                BalanceOp::Deposit(_, v, _) => {
3192                                    if !v.is_zero() && *v > reaped_till {
3193                                        break;
3194                                    } else {
3195                                        drained = true;
3196                                        break;
3197                                    }
3198                                }
3199                                BalanceOp::Reap(v, _) => reaped_till += v,
3200                                BalanceOp::Mint(v, _) => {
3201                                    if !v.is_zero() && *v > reaped_till {
3202                                        break;
3203                                    } else {
3204                                        drained = true;
3205                                        break;
3206                                    }
3207                                }
3208                                _ => {}
3209                            }
3210                        }
3211                        if drained && *v == u128::MAX {
3212                            return false;
3213                        }
3214                        true
3215                    }
3216                    _ => true,
3217                },
3218
3219                reason: format!("{:?}", ShareBalanceError::AssetOverflow),
3220            };
3221
3222            let adjustments = {
3223                let mut v = STRESS_ADJUSTMENTS.to_vec();
3224                v.push(u128::MAX);
3225                v
3226            };
3227
3228            Tester::explore_traps(
3229                &USERS,
3230                STRESS_DEPOSITS,
3231                &adjustments,
3232                SUBJECTS,
3233                MAX_DEPTH,
3234                STRESS_BPS,
3235                STRESS_DIFF,
3236                Some(traps),
3237                &mut results,
3238            );
3239
3240            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3241        }
3242
3243        #[test]
3244        #[ignore]
3245        fn trap_manual_collapse() {
3246            let mut results = Tester::initiate_results();
3247
3248            let traps: TrapConfig = BalanceTraps {
3249                trap: |state, op| match op {
3250                    BalanceOp::Mint(v, _) => {
3251                        let balance = &state.lazy.balance;
3252                        let effective = balance::effective::<MockShareBalance>(balance);
3253                        let bias = balance::bias::<MockShareBalance>(balance);
3254
3255                        if effective.is_none() && bias.is_none() {
3256                            return false;
3257                        }
3258
3259                        let zero_value = v.is_zero();
3260
3261                        let overflow = v.checked_add(&effective.unwrap()).is_none();
3262
3263                        let no_deposits = state.receipts.is_empty();
3264
3265                        let zero_manual = state.manual.total_fixed().is_zero();
3266
3267                        let manual_no_users = state.manual.users.is_empty();
3268
3269                        let manual_drain_shares = state.manual.before_drain.is_some();
3270
3271                        let manual_collapse =
3272                            !manual_no_users && zero_manual && !manual_drain_shares;
3273
3274                        !zero_value && !overflow && !no_deposits && manual_collapse
3275                    }
3276                    _ => false,
3277                },
3278
3279                flow: |_, _| true,
3280
3281                reason: format!("{:?}", ManualError::CollapsedState),
3282            };
3283
3284            Tester::explore_traps(
3285                &USERS,
3286                STRESS_DEPOSITS,
3287                STRESS_ADJUSTMENTS,
3288                SUBJECTS,
3289                (MAX_DEPTH + 2).min(10),
3290                STRESS_BPS,
3291                STRESS_DIFF,
3292                Some(traps),
3293                &mut results,
3294            );
3295
3296            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3297        }
3298
3299        // ===============================================================================
3300        // `````````````````````````````` TRAP-CHECKS (REAP) `````````````````````````````
3301        // ===============================================================================
3302
3303        #[test]
3304        #[ignore]
3305        fn trap_reap_underflow() {
3306            let mut results = Tester::initiate_results();
3307
3308            let traps: TrapConfig = BalanceTraps {
3309                trap: |state, op| match op {
3310                    BalanceOp::Reap(v, _) => {
3311                        let balance = &state.lazy.balance;
3312                        let effective = balance::effective::<MockShareBalance>(balance);
3313                        let bias = balance::bias::<MockShareBalance>(balance);
3314
3315                        if effective.is_none() && bias.is_none() {
3316                            return false;
3317                        }
3318
3319                        let no_deposits = state.receipts.is_empty();
3320
3321                        let lazy_underflow = effective.unwrap().checked_sub(*v).is_none();
3322
3323                        let manual_underflow = state.manual.total().checked_sub(*v).is_none();
3324
3325                        !no_deposits && lazy_underflow && manual_underflow
3326                    }
3327                    _ => false,
3328                },
3329
3330                flow: |_, _| true,
3331
3332                reason: format!("{:?}", ShareBalanceError::AssetUnderflow),
3333            };
3334
3335            Tester::explore_traps(
3336                &USERS,
3337                STRESS_DEPOSITS,
3338                STRESS_ADJUSTMENTS,
3339                SUBJECTS,
3340                MAX_DEPTH,
3341                STRESS_BPS,
3342                STRESS_DIFF,
3343                Some(traps),
3344                &mut results,
3345            );
3346
3347            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3348        }
3349
3350        #[test]
3351        #[ignore]
3352        fn trap_reap_without_deposits() {
3353            let mut results = Tester::initiate_results();
3354
3355            let traps: TrapConfig = BalanceTraps {
3356                trap: |state, op| match op {
3357                    BalanceOp::Reap(v, _) => {
3358                        let balance = &state.lazy.balance;
3359                        let effective = balance::effective::<MockShareBalance>(balance);
3360                        let bias = balance::bias::<MockShareBalance>(balance);
3361
3362                        if effective.is_none() && bias.is_none() {
3363                            return false;
3364                        }
3365
3366                        let no_deposits = state.receipts.is_empty();
3367
3368                        let zero_value = v.is_zero();
3369
3370                        no_deposits && !zero_value
3371                    }
3372                    _ => false,
3373                },
3374
3375                flow: |_, _| true,
3376
3377                reason: format!("{:?}", ShareBalanceError::RequiresExistingDeposits),
3378            };
3379
3380            Tester::explore_traps(
3381                &USERS,
3382                STRESS_DEPOSITS,
3383                STRESS_ADJUSTMENTS,
3384                SUBJECTS,
3385                MAX_DEPTH,
3386                STRESS_BPS,
3387                STRESS_DIFF,
3388                Some(traps),
3389                &mut results,
3390            );
3391
3392            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3393        }
3394
3395        #[test]
3396        #[ignore]
3397        fn trap_manual_zero_reap_without_deposits() {
3398            let mut results = Tester::initiate_results();
3399
3400            let traps: TrapConfig = BalanceTraps {
3401                trap: |state, op| match op {
3402                    BalanceOp::Reap(v, _) => {
3403                        let balance = &state.lazy.balance;
3404                        let effective = balance::effective::<MockShareBalance>(balance);
3405                        let bias = balance::bias::<MockShareBalance>(balance);
3406
3407                        if effective.is_none() && bias.is_none() {
3408                            return false;
3409                        }
3410
3411                        let no_deposits = state.receipts.is_empty();
3412
3413                        let zero_value = v.is_zero();
3414
3415                        no_deposits && zero_value
3416                    }
3417                    _ => false,
3418                },
3419
3420                flow: |_, _| true,
3421
3422                reason: format!("{:?}", ManualError::ReapWithoutDeposits),
3423            };
3424
3425            Tester::explore_traps(
3426                &USERS,
3427                STRESS_DEPOSITS,
3428                STRESS_ADJUSTMENTS,
3429                SUBJECTS,
3430                MAX_DEPTH,
3431                STRESS_BPS,
3432                STRESS_DIFF,
3433                Some(traps),
3434                &mut results,
3435            );
3436
3437            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3438        }
3439
3440        #[test]
3441        #[ignore]
3442        fn trap_reap_fresh_balance() {
3443            let mut results = Tester::initiate_results();
3444
3445            let traps: TrapConfig = BalanceTraps {
3446                trap: |state, op| match op {
3447                    BalanceOp::Reap(v, _) => {
3448                        let balance = &state.lazy.balance;
3449                        let effective = balance::effective::<MockShareBalance>(balance);
3450                        let bias = balance::bias::<MockShareBalance>(balance);
3451
3452                        let fresh = effective.is_none() && bias.is_none();
3453
3454                        let zero_value = v.is_zero();
3455
3456                        fresh & !zero_value
3457                    }
3458                    _ => false,
3459                },
3460
3461                flow: |_, _| true,
3462
3463                reason: format!("{:?}", ShareBalanceError::BalanceNotInitiatedViaDeposit),
3464            };
3465
3466            Tester::explore_traps(
3467                &USERS,
3468                STRESS_DEPOSITS,
3469                STRESS_ADJUSTMENTS,
3470                SUBJECTS,
3471                MAX_DEPTH,
3472                STRESS_BPS,
3473                STRESS_DIFF,
3474                Some(traps),
3475                &mut results,
3476            );
3477
3478            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3479        }
3480
3481        #[test]
3482        #[ignore]
3483        fn trap_manual_zero_reap_fresh_balance() {
3484            let mut results = Tester::initiate_results();
3485
3486            let traps: TrapConfig = BalanceTraps {
3487                trap: |state, op| match op {
3488                    BalanceOp::Reap(v, _) => {
3489                        let balance = &state.lazy.balance;
3490                        let effective = balance::effective::<MockShareBalance>(balance);
3491                        let bias = balance::bias::<MockShareBalance>(balance);
3492
3493                        let fresh = effective.is_none() && bias.is_none();
3494
3495                        let zero_value = v.is_zero();
3496
3497                        fresh & zero_value
3498                    }
3499                    _ => false,
3500                },
3501
3502                flow: |state, op| match op {
3503                    BalanceOp::Reap(..) => {
3504                        if state.trace.len().is_zero() {
3505                            true
3506                        } else {
3507                            false
3508                        }
3509                    }
3510                    _ => false,
3511                },
3512
3513                reason: format!("{:?}", ManualError::ReapWithoutDeposits),
3514            };
3515
3516            Tester::explore_traps(
3517                &USERS,
3518                STRESS_DEPOSITS,
3519                &[0],
3520                SUBJECTS,
3521                (MAX_DEPTH + 2).min(10),
3522                STRESS_BPS,
3523                STRESS_DIFF,
3524                Some(traps),
3525                &mut results,
3526            );
3527
3528            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3529        }
3530
3531        // ===============================================================================
3532        // ````````````````````````````` TRAP-CHECKS (DRAIN) `````````````````````````````
3533        // ===============================================================================
3534
3535        #[test]
3536        #[ignore]
3537        fn trap_manual_zero_drain() {
3538            let mut results = Tester::initiate_results();
3539
3540            let traps: TrapConfig = BalanceTraps {
3541                trap: |state, op| match op {
3542                    BalanceOp::Drain => {
3543                        let balance = &state.lazy.balance;
3544                        let effective = balance::effective::<MockShareBalance>(balance);
3545                        let bias = balance::bias::<MockShareBalance>(balance);
3546
3547                        if effective.is_none() && bias.is_none() {
3548                            return true;
3549                        }
3550
3551                        let zero_effective = effective.unwrap().is_zero();
3552
3553                        zero_effective
3554                    }
3555                    _ => false,
3556                },
3557
3558                flow: |_, _| true,
3559
3560                reason: format!("{:?}", ManualError::DrainWithoutDeposits),
3561            };
3562
3563            Tester::explore_traps(
3564                &USERS,
3565                STRESS_DEPOSITS,
3566                STRESS_ADJUSTMENTS,
3567                SUBJECTS,
3568                MAX_DEPTH,
3569                STRESS_BPS,
3570                STRESS_DIFF,
3571                Some(traps),
3572                &mut results,
3573            );
3574
3575            Tester::write_reports(results_dir("trap_check"), &results, false, false, false);
3576        }
3577
3578        // ===============================================================================
3579        // ````````````````````````` MODEL-CHECKER UTILITY IMPLS `````````````````````````
3580        // ===============================================================================
3581
3582        /// Concrete test harness implementing [`LazyBalanceModelChecker`].
3583        struct Tester;
3584
3585        impl LazyBalanceModelChecker for Tester {
3586            /// The lazy (optimized) balance model under test.
3587            type LazyBalance = MockShareBalance;
3588
3589            /// The manual/reference implementation used for verification.
3590            type ManualBalance = ManualBalance<MockShareBalance>;
3591
3592            /// Predicate that detects invalid or trap states.
3593            type TrapFn = fn(
3594                &BalanceState<Self::LazyBalance, Self::ManualBalance>,
3595                &BalanceOp<Self::LazyBalance, Self::ManualBalance>,
3596            ) -> bool;
3597
3598            /// Predicate that validates whether a state transition is allowed.
3599            type FlowFn = fn(
3600                &BalanceState<Self::LazyBalance, Self::ManualBalance>,
3601                &BalanceOp<Self::LazyBalance, Self::ManualBalance>,
3602            ) -> bool;
3603
3604            /// Additional Hashing function used to identify or deduplicate states.
3605            ///
3606            /// Although not utilized in current test-cases.
3607            type Hasher = fn(&BalanceState<Self::LazyBalance, Self::ManualBalance>) -> u64;
3608        }
3609
3610        /// Configuration type for balance trap handling.
3611        ///
3612        /// Combines:
3613        /// - Trap predicate ([`LazyBalanceModelChecker::TrapFn`]) detects invalid states
3614        /// - Flow predicate ([`LazyBalanceModelChecker::FlowFn`]) validates allowed op-sequences
3615        type TrapConfig = BalanceTraps<
3616            <Tester as LazyBalanceModelChecker>::TrapFn,
3617            <Tester as LazyBalanceModelChecker>::FlowFn,
3618        >;
3619
3620        /// Simple User type from a given [`ManualBalanceModel::User`].
3621        type User<T> = <ManualBalance<T> as ManualBalanceModel<T>>::User;
3622
3623        /// Asset type associated from [`LazyBalance::Asset`].
3624        type AssetOf<T> = <T as LazyBalance>::Asset;
3625
3626        /// Receipt (deposit bill) type associated from [`LazyBalance::Receipt`].
3627        type ReceiptOf<T> = <T as LazyBalance>::Receipt;
3628
3629        /// Hashes a [`BalanceState`] based on its execution trace (sequence of operations),
3630        /// producing a compact identifier for path-based state exploration.
3631        ///
3632        /// ## Model Context
3633        /// In our share balance models ([`ShareBalanceFamily`]), all operations are
3634        /// **value-insensitive*, their correctness depends only on the sequence and type
3635        /// of operations, not on the specific numeric values involved.
3636        ///
3637        /// Because of this, we intentionally hash only the **operation trace**
3638        /// (`state.trace`) and ignore parameters.
3639        ///
3640        /// ## Design choice
3641        /// This is a **path-based hashing strategy**, not a full state hash:
3642        /// - Efficient and lightweight
3643        /// - Correct for value-insensitive models like [`ShareBalanceFamily`]
3644        /// - Does NOT distinguish different parameter values
3645        ///
3646        /// ## Example
3647        /// ```ignore
3648        /// // First exploration:
3649        /// Deposit(10) -> Withdraw(5)
3650        /// -> trace: [Deposit, Withdraw]
3651        /// -> hash stored
3652        ///
3653        /// // Later:
3654        /// Deposit(1000) -> Withdraw(1)
3655        /// -> trace: [Deposit, Withdraw]
3656        /// -> same hash -> skipped (already explored)
3657        /// ```
3658        impl<T, M> BalanceStateHasher<T, M> for ManualBalance<T>
3659        where
3660            T: LazyBalance + Clone + Debug,
3661            M: ManualBalanceModel<T>,
3662            M::User: Hash,
3663        {
3664            /// Computes a hash for the given state based solely on its operation trace.
3665            ///
3666            /// Each operation contributes a fixed discriminator:
3667            /// - Deposit -> 0
3668            /// - Withdraw -> 1
3669            /// - Mint -> 2
3670            /// - Reap -> 3
3671            /// - Drain -> 4
3672            ///
3673            /// The resulting hash uniquely identifies the **sequence of operations**
3674            /// (not the resulting balances), and is used by the model checker to
3675            /// detect and skip already-explored execution paths.
3676            fn hash(state: &BalanceState<T, M>) -> u64 {
3677                let mut h = DefaultHasher::new();
3678
3679                for op in &state.trace {
3680                    match op {
3681                        BalanceOp::Deposit(..) => {
3682                            0u8.hash(&mut h);
3683                        }
3684                        BalanceOp::Withdraw(_) => {
3685                            1u8.hash(&mut h);
3686                        }
3687                        BalanceOp::Mint(..) => {
3688                            2u8.hash(&mut h);
3689                        }
3690                        BalanceOp::Reap(..) => {
3691                            3u8.hash(&mut h);
3692                        }
3693                        BalanceOp::Drain => {
3694                            4u8.hash(&mut h);
3695                        }
3696                    }
3697                }
3698
3699                h.finish()
3700            }
3701        }
3702
3703        /// Guard implementation for validating balance operations of [`ShareBalanceFamily`]
3704        /// and its manual model [`ManualBalance`].
3705        ///
3706        /// These guards define whether a given operation is **allowed to proceed**
3707        /// from the current [`BalanceState`]. They act as preconditions that ensure
3708        /// only valid transitions are explored during model checking.
3709        impl<T> BalanceGuards<T, ManualBalance<T>> for ManualBalance<T>
3710        where
3711            T: LazyBalance + Clone + Debug,
3712            <T as LazyBalance>::Asset: From<u128>,
3713        {
3714            /// Validates whether a deposit operation is allowed for the given state.
3715            ///
3716            /// ## Checks performed
3717            /// - Rejects deposits into a **drained state**
3718            /// - Prevents **duplicate deposits** for the same user
3719            /// - Disallows **zero-value deposits**
3720            /// - Ensures deposit is large enough to produce a valid share
3721            /// - Prevents arithmetic failures during ratio derivation
3722            ///
3723            /// ## Behavior
3724            /// - If the system is uninitialized (no effective, bias, issued),
3725            ///   any non-zero deposit is allowed.
3726            ///
3727            /// ## Returns
3728            /// - `true`: [`BalanceOp::Deposit`] is valid and can be applied
3729            /// - `false`: deposit is invalid and should be skipped
3730            fn deposit(
3731                state: &BalanceState<T, ManualBalance<T>>,
3732                user: &User<T>,
3733                amount: &T::Asset,
3734                _subject: &T::Subject,
3735            ) -> bool {
3736                let balance = &state.lazy.balance;
3737                let effective = balance::effective::<T>(balance);
3738                let bias = balance::bias::<T>(balance);
3739                let issued = balance::issued::<T>(balance);
3740
3741                // Initial state: allow any non-zero deposit (Fast-track)
3742                if effective.is_none() && bias.is_none() && issued.is_none() {
3743                    if amount.is_zero() {
3744                        return false;
3745                    }
3746                    return true;
3747                }
3748
3749                // State is considered drained if both effective and bias are zero
3750                let drained = effective.unwrap().is_zero() && bias.unwrap().is_zero();
3751
3752                // Prevent duplicate deposits from the same user
3753                let duplicate = state.receipts.contains_key(user);
3754
3755                // Reject zero-value deposits
3756                let zero_deposit = amount.is_zero();
3757
3758                // Reject deposits that would result in zero share issuance
3759                let zero_share = {
3760                    match issued.unwrap().is_zero() {
3761                        true => false,
3762                        false => {
3763                            let adjusted = issued.unwrap().saturating_sub(One::one());
3764                            match effective.unwrap().checked_add(&adjusted) {
3765                                Some(total) => {
3766                                    let min_required = total / issued.unwrap();
3767                                    *amount < min_required
3768                                }
3769                                None => false,
3770                            }
3771                        }
3772                    }
3773                };
3774
3775                // Prevent failure in rational derivation (e.g. division by zero)
3776                let derive_fail = T::Rational::saturating_from_integer(*amount)
3777                    .checked_div(&bias.unwrap())
3778                    .is_none();
3779
3780                !drained && !duplicate && !zero_deposit && !zero_share && !derive_fail
3781            }
3782
3783            /// Validates whether a withdraw operation is allowed for the given state.
3784            ///
3785            /// ## Checks performed
3786            /// - Ensures the user has an existing receipt (i.e. has previously deposited)
3787            /// (tracked via `state.receipts`).
3788            ///
3789            /// ## Returns
3790            /// - `true`: [`BalanceOp::Withdraw`] is valid and can be applied
3791            /// - `false`: withdraw is invalid and should be skipped
3792            fn withdraw(state: &BalanceState<T, ManualBalance<T>>, user: &User<T>) -> bool {
3793                let exists = state.receipts.contains_key(user);
3794                exists
3795            }
3796
3797            /// Validates whether a mint operation is allowed for the given state.
3798            ///
3799            /// ## Checks performed
3800            /// - Rejects minting in an uninitialized state (no effective or bias)
3801            /// - Disallows zero-value mint operations
3802            /// - Ensures there is at least one active deposit (non-empty receipts)
3803            /// - Prevents arithmetic overflow when updating effective balance
3804            /// - Avoids inconsistent manual model states (e.g. collapsed shares)
3805            ///
3806            /// ## Returns
3807            /// - `true`: [`BalanceOp::Mint`] is valid and can be applied
3808            /// - `false`: mint is invalid and should be skipped
3809            fn mint(
3810                state: &BalanceState<T, ManualBalance<T>>,
3811                value: &T::Asset,
3812                _subject: &T::Subject,
3813            ) -> bool {
3814                let balance = &state.lazy.balance;
3815                let effective = balance::effective::<T>(balance);
3816                let bias = balance::bias::<T>(balance);
3817
3818                // Reject if system is not initialized
3819                if effective.is_none() && bias.is_none() {
3820                    return false;
3821                }
3822
3823                let zero_value = value.is_zero();
3824
3825                // No deposits -> nothing to mint against
3826                let no_deposits = state.receipts.is_empty();
3827
3828                // Prevent overflow when increasing effective balance
3829                let overflow = value.checked_add(&effective.unwrap()).is_none();
3830
3831                // Manual model consistency checks
3832                let zero_manual = state.manual.total_fixed().is_zero();
3833                let manual_no_users = state.manual.users.is_empty();
3834                let manual_drain_shares = state.manual.before_drain.is_some();
3835
3836                // Detect collapsed manual state (invalid share distribution)
3837                let manual_collapse = !manual_no_users && zero_manual && !manual_drain_shares;
3838
3839                !zero_value && !no_deposits && !overflow && !manual_collapse
3840            }
3841
3842            /// Validates whether a reap operation is allowed for the given state.
3843            ///
3844            /// ## Checks performed
3845            /// - Rejects reaping in an uninitialized state (no effective or bias)
3846            /// - Disallows zero-value reap operations
3847            /// - Ensures there is at least one active deposit (non-empty receipts)
3848            /// - Prevents underflow in the lazy model (effective balance)
3849            /// - Prevents underflow in the manual model (total balance)
3850            ///
3851            /// ## Behavior
3852            /// - Reaping is only allowed when the system is initialized and active
3853            /// - The operation must not reduce balances below zero (underflow) in
3854            /// either model
3855            ///
3856            /// ## Returns
3857            /// - `true`: [`BalanceOp::Reap`] is valid and can be applied
3858            /// - `false`: reap is invalid and should be skipped
3859            fn reap(
3860                state: &BalanceState<T, ManualBalance<T>>,
3861                value: &T::Asset,
3862                _subject: &T::Subject,
3863            ) -> bool {
3864                let balance = &state.lazy.balance;
3865                let effective = balance::effective::<T>(balance);
3866                let bias = balance::bias::<T>(balance);
3867
3868                // Reject if system is not initialized
3869                if effective.is_none() && bias.is_none() {
3870                    return false;
3871                }
3872
3873                let zero_value = value.is_zero();
3874
3875                // No deposits -> nothing to reap from
3876                let no_deposits = state.receipts.is_empty();
3877
3878                // Prevent underflow in lazy model
3879                let lazy_underflow = effective.unwrap().checked_sub(value).is_none();
3880
3881                // Prevent underflow in manual model
3882                let manual_underflow = state.manual.total().checked_sub(value).is_none();
3883
3884                !zero_value && !no_deposits && !lazy_underflow && !manual_underflow
3885            }
3886
3887            /// Validates whether a drain operation is allowed for the given state.
3888            ///
3889            /// ## Checks performed
3890            /// - Ensures there is at least one active deposit (non-empty receipts)
3891            ///
3892            /// ## Behavior
3893            /// - Drain is only meaningful when there are existing deposits to clear
3894            /// - If no users have deposited, the operation is skipped
3895            ///
3896            /// ## Returns
3897            /// - `true`: [`BalanceOp::Drain`] is valid and can be applied
3898            /// - `false`: drain is invalid and should be skipped
3899            fn drain(state: &BalanceState<T, ManualBalance<T>>) -> bool {
3900                let no_deposits = state.receipts.is_empty();
3901                !no_deposits
3902            }
3903
3904            /// Validates core invariants of the balance state.
3905            ///
3906            /// ## Invariant enforced
3907            /// - If there are active deposits (i.e. receipts exist),
3908            ///   then the total shares (`issued`) must be non-zero.
3909            ///
3910            /// ## Rationale
3911            /// In the [`ShareBalanceFamily`] model:
3912            /// - Deposits correspond to issued shares
3913            /// - Therefore, the existence of deposits implies that shares must exist
3914            ///
3915            /// A state where:
3916            /// - deposits exist, but
3917            /// - total shares are zero
3918            ///
3919            /// is considered invalid and indicates a broken or collapsed system state.
3920            ///
3921            /// ## Behavior
3922            /// - If no deposits exist, then invariant is trivially satisfied
3923            /// - If deposits exist:
3924            ///   - Ensures the `issued` (total shares) field is present
3925            ///   - Ensures the `issued` is non-zero
3926            fn invariant(state: &BalanceState<T, ManualBalance<T>>) -> Result<(), String> {
3927                // If Deposits Exists, Total Shares of Balance cannot be zero
3928                if !state.receipts.is_empty() {
3929                    let balance = &state.lazy.balance;
3930                    let repr = <T::Balance as VirtualDynField<BalanceAsset>>::access(balance);
3931                    let vec = IntoTag::<_, ManyTag>::into_tag(repr);
3932                    let Some(principal) = vec.as_ref().get(1) else {
3933                        return Err("Invariant::TotalSharesMissing".to_string());
3934                    };
3935                    if *principal == Zero::zero() {
3936                        return Err("Invariant::ZeroTotalShares".to_string());
3937                    }
3938                }
3939                Ok(())
3940            }
3941        }
3942
3943        // ===============================================================================
3944        // ```````````````````````````````` MANUAL BALANCE ```````````````````````````````
3945        // ===============================================================================
3946
3947        /// A simple counter-based User-ID wrapper
3948        #[derive(
3949            Encode,
3950            Decode,
3951            Debug,
3952            Clone,
3953            Copy,
3954            Hash,
3955            PartialEq,
3956            Eq,
3957            PartialOrd,
3958            Ord,
3959            DecodeWithMemTracking,
3960            MaxEncodedLen,
3961            TypeInfo,
3962        )]
3963        struct UserID(u32);
3964
3965        /// Manual (reference) balance model used for verification against the lazy model.
3966        ///
3967        /// This struct maintains an explicit, user-level representation of balances,
3968        /// serving as a **ground truth** for validating correctness of [`ShareBalanceFamily`]
3969        /// lazy balance model.
3970        ///
3971        /// ## Representation
3972        /// - User balances are stored as [`FixedU128`] with maximum precision
3973        /// - When interacting with the lazy model, values are **floored** to the
3974        ///   underlying asset type ([`LazyBalance::Asset`])
3975        /// - No explicit "share" abstraction exists here, to assume subjectively:
3976        ///   - Each user balance directly represents their proportional ownership
3977        ///   - The total balance represents the total capital (i.e. sum of all shares)
3978        ///
3979        /// ## Ledger Model (Intuition)
3980        /// This model behaves like a **ledger book**:
3981        /// - Every operation (e.g. `mint`, `reap`) is immediately reflected across
3982        ///   all user balances
3983        /// - Each user's balance always represents their up-to-date proportional ownership
3984        ///
3985        /// This makes the system:
3986        /// - Simple and explicit
3987        /// - Easy to reason about
3988        /// - Straightforward to validate
3989        ///
3990        /// In contrast, the [`LazyBalance`] uses **deferred receipt-based accounting**,
3991        /// where withdrawals are derived indirectly. This manual model provides a
3992        /// clear baseline to verify those deferred computations.
3993        ///
3994        /// ## Drain / Revival Semantics
3995        /// - A `drain` operation removes all balances (system reset, no shares remain)
3996        /// - Before draining, user balances are stored in `before_drain`
3997        /// - When the system is revived (e.g. via `mint`):
3998        ///   - Balances are **reconstructed proportionally**
3999        ///   - Distribution is based on the pre-drain snapshot
4000        ///   - This ensures continuity of ownership without explicit share tracking
4001        #[derive(Clone, Debug, PartialEq, Eq)]
4002        struct ManualBalance<T>
4003        where
4004            T: LazyBalance,
4005        {
4006            /// Mapping of users to their high-precision proportional balances.
4007            ///
4008            /// Each value represents the user's share of total capital directly,
4009            /// without a separate share abstraction.
4010            users: BTreeMap<UserID, FixedU128>,
4011
4012            /// Snapshot of user balances before a drain operation.
4013            ///
4014            /// Used to restore proportional ownership when the system is
4015            /// revived and immediately prunes itself to `None`.
4016            before_drain: Option<BTreeMap<UserID, FixedU128>>,
4017
4018            /// Marker for [`LazyBalance`].
4019            _marker: PhantomData<T>,
4020        }
4021
4022        impl<T> ManualBalance<T>
4023        where
4024            T: LazyBalance,
4025        {
4026            /// Creates an empty manual balance state.
4027            fn new() -> Self {
4028                Self {
4029                    users: BTreeMap::new(),
4030                    before_drain: None,
4031                    _marker: PhantomData,
4032                }
4033            }
4034
4035            /// Returns the total balance across all users (high-precision)
4036            ///
4037            /// [`ManualBalanceModel::total`] may utilize this and floor to give a unsigned
4038            /// asset balance value.
4039            fn total_fixed(&self) -> FixedU128 {
4040                self.users.values().fold(FixedU128::zero(), |a, b| a + *b)
4041            }
4042        }
4043        /// Errors representing invalid operations or states in the manual balance model.
4044        #[derive(Clone, Debug, PartialEq, Eq)]
4045        pub enum ManualError {
4046            /// Deposit attempted for a user that already has a position.
4047            DuplicateDeposit,
4048
4049            /// Mint attempted when no deposits exist.
4050            MintWithoutDeposits,
4051
4052            /// Reap attempted when no deposits exist.
4053            ReapWithoutDeposits,
4054
4055            /// Drain attempted when no deposits exist.
4056            DrainWithoutDeposits,
4057
4058            /// Deposit with zero value is not allowed.
4059            ZeroDeposit,
4060
4061            /// Withdraw attempted after a drain without a valid
4062            /// `before_drain` snapshot to restore balances.
4063            WithdrawAfterDrainedSnapshotNotFound,
4064
4065            /// Withdraw attempted by a user with no recorded deposit.
4066            ///
4067            /// In the lazy model, withdrawals operate on receipts without explicit
4068            /// user identity. The manual model requires a corresponding user entry,
4069            /// so a missing deposit makes the operation invalid.
4070            WithdrawWithoutDeposit,
4071
4072            /// Invalid collapsed state where users exist but total balance is zero.
4073            ///
4074            /// This can occur due to precision differences between:
4075            /// - lazy model (share-based, integer)
4076            /// - manual model (fixed-point, proportional)
4077            ///
4078            /// In extreme cases, a "silent full reap" may occur:
4079            /// - all value is effectively removed
4080            /// - but no `before_drain` snapshot was captured
4081            ///
4082            /// This creates ambiguity, as the system appears drained without
4083            /// explicit drain semantics.
4084            ///
4085            /// [`ShareBalanceFamily`] handles this internally, but the manual model
4086            /// cannot reliably detect it without duplicating core logic. Hence,
4087            /// this condition is surfaced as an error and mitigated via guards/traps.
4088            CollapsedState,
4089        }
4090
4091        impl<T> ManualBalanceModel<T> for ManualBalance<T>
4092        where
4093            T: LazyBalanceMarker,
4094            <T as LazyBalance>::Asset: From<u128>,
4095        {
4096            /// User identifier type.
4097            type User = UserID;
4098
4099            /// Creates a new empty manual balance model.
4100            fn new() -> Self {
4101                ManualBalance::new()
4102            }
4103
4104            /// Returns total balance floored to asset representation.
4105            fn total(&self) -> AssetOf<T> {
4106                let total_fixed = self.total_fixed();
4107                (total_fixed.into_inner() / FixedU128::DIV).into()
4108            }
4109
4110            /// Error type for manual model operations.
4111            type Error = ManualError;
4112
4113            /// Adds a new user with an initial balance.
4114            ///
4115            /// - Rejects duplicate users
4116            /// - Rejects zero deposits
4117            fn deposit(
4118                &mut self,
4119                id: Self::User,
4120                amount: AssetOf<T>,
4121                _lazy: &(AssetOf<T>, ReceiptOf<T>),
4122            ) -> Result<(), Self::Error> {
4123                if self.users.contains_key(&id) {
4124                    return Err(ManualError::DuplicateDeposit);
4125                }
4126
4127                if amount.is_zero() {
4128                    return Err(ManualError::ZeroDeposit);
4129                }
4130
4131                self.users
4132                    .insert(id, FixedU128::saturating_from_integer(amount));
4133
4134                self.before_drain = None;
4135
4136                Ok(())
4137            }
4138
4139            /// Removes a user and returns their balance.
4140            ///
4141            /// - Validates existence of user
4142            /// - Handles post-drain snapshot consistency
4143            fn withdraw(
4144                &mut self,
4145                id: Self::User,
4146                _lazy: &AssetOf<T>,
4147            ) -> Result<AssetOf<T>, Self::Error> {
4148                if let Some(snapshot) = &mut self.before_drain {
4149                    snapshot
4150                        .remove(&id)
4151                        .ok_or(ManualError::WithdrawAfterDrainedSnapshotNotFound)?;
4152                }
4153
4154                let fixed = self
4155                    .users
4156                    .remove(&id)
4157                    .ok_or(ManualError::WithdrawWithoutDeposit)?;
4158
4159                Ok((fixed.into_inner() / FixedU128::DIV).into())
4160            }
4161
4162            /// Distributes value proportionally across all users.
4163            ///
4164            /// - Requires existing deposits
4165            /// - Uses proportional distribution based on current balances
4166            /// - If total is zero (post-drain), uses `before_drain` snapshot
4167            fn mint(&mut self, value: AssetOf<T>, _lazy: &AssetOf<T>) -> Result<(), Self::Error> {
4168                if self.users.is_empty() {
4169                    return Err(ManualError::MintWithoutDeposits);
4170                }
4171
4172                if value.is_zero() {
4173                    return Ok(());
4174                }
4175
4176                let total_before = self.total_fixed();
4177                let v = FixedU128::saturating_from_integer(value);
4178
4179                // Revival path after drain
4180                if total_before.is_zero() {
4181                    let shares = self
4182                        .before_drain
4183                        .as_ref()
4184                        .ok_or(ManualError::CollapsedState)?;
4185
4186                    let total_shares = shares.values().fold(Zero::zero(), |a: FixedU128, b| a + *b);
4187
4188                    if total_shares.is_zero() {
4189                        return Ok(());
4190                    }
4191
4192                    for (id, bal) in self.users.iter_mut() {
4193                        let weight = shares.get(id).cloned().unwrap_or_default();
4194                        let gain = (weight / total_shares) * v;
4195                        *bal = *bal + gain;
4196                    }
4197
4198                    return Ok(());
4199                }
4200
4201                // Normal proportional mint
4202                for bal in self.users.values_mut() {
4203                    let gain = (*bal / total_before) * v;
4204                    *bal = *bal + gain;
4205                }
4206
4207                self.before_drain = None;
4208
4209                Ok(())
4210            }
4211
4212            /// Removes value proportionally from all users.
4213            ///
4214            /// - Requires existing deposits
4215            /// - Performs proportional reduction
4216            /// - Converts to drain if full depletion
4217            fn reap(&mut self, value: AssetOf<T>, _lazy: &AssetOf<T>) -> Result<(), Self::Error> {
4218                if self.users.is_empty() {
4219                    return Err(ManualError::ReapWithoutDeposits);
4220                }
4221
4222                if value.is_zero() {
4223                    return Ok(());
4224                }
4225
4226                let total_before = self.total_fixed();
4227                let total_int = (total_before.into_inner() / FixedU128::DIV).into();
4228
4229                // Full reap -> drain
4230                if value >= total_int {
4231                    self.drain()?;
4232                    return Ok(());
4233                }
4234
4235                let v = FixedU128::saturating_from_integer(value);
4236
4237                for bal in self.users.values_mut() {
4238                    let loss = (*bal / total_before) * v;
4239                    *bal = *bal - loss;
4240                }
4241
4242                Ok(())
4243            }
4244
4245            /// Resets all balances to zero while preserving proportional snapshot.
4246            ///
4247            /// - Stores pre-drain balances in `before_drain`
4248            /// - Used for later proportional revival via mint
4249            fn drain(&mut self) -> Result<(), Self::Error> {
4250                if self.users.is_empty() {
4251                    return Err(ManualError::DrainWithoutDeposits);
4252                }
4253
4254                if self.total_fixed().is_zero() {
4255                    return Ok(());
4256                }
4257
4258                self.before_drain = Some(self.users.clone());
4259
4260                for bal in self.users.values_mut() {
4261                    *bal = FixedU128::zero();
4262                }
4263
4264                Ok(())
4265            }
4266        }
4267    }
4268
4269    // ===============================================================================
4270    // ````````````````````````` LAZY BALANCE MOCK PROVIDERS ````````````````````````
4271    // ===============================================================================
4272
4273    #[cfg(test)]
4274    /// Mock Providers implementing [`LazyBalance`] using [`ShareBalanceFamily`]
4275    mod mock {
4276
4277        // ===============================================================================
4278        // ``````````````````````````````````` IMPORTS ```````````````````````````````````
4279        // ===============================================================================
4280
4281        // --- Local ---
4282        use super::*;
4283
4284        // --- Scale / codec ---
4285        use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
4286        use scale_info::TypeInfo;
4287
4288        // --- FRAME Suite ---
4289        use frame_suite::{misc::Extent, plugin_context};
4290
4291        // --- FRAME Support ---
4292        use frame_support::{
4293            pallet_prelude::NMapKey,
4294            storage::types::{OptionQuery, StorageNMap},
4295            traits::StorageInstance,
4296            Blake2_128Concat,
4297        };
4298
4299        // --- Substrate ---
4300        use sp_core::ConstU32;
4301        use sp_runtime::FixedU128;
4302
4303        // --- std ---
4304        use std::{borrow::Cow, cell::RefCell, collections::BTreeMap, marker::PhantomData};
4305
4306        // ===============================================================================
4307        // `````````````````````````````` MOCK SHARE BALANCE `````````````````````````````
4308        // ===============================================================================
4309
4310        #[derive(Debug, Clone, Default)]
4311        /// Implements mock [`LazyBalance`] by utilizing [`ShareBalanceFamily`]
4312        pub struct MockShareBalance;
4313
4314        impl LazyBalance for MockShareBalance {
4315            type Asset = u128;
4316            type Rational = FixedU128;
4317            type Time = u32;
4318            type Variant = u8;
4319            type Id = u8;
4320            type Subject = TestSubject;
4321
4322            type Balance = TestBalance;
4323            type SnapShot = TestSnapshot;
4324            type Receipt = TestReceipt;
4325            type Limits = TestLimit;
4326
4327            type Input<'a> = TestInput<'a>;
4328            type Output<'a> = TestOutput<'a>;
4329
4330            type BalanceContext = MyShareBalance<Self>;
4331            type BalanceFamily<'a> = ShareBalanceFamily<'a>;
4332        }
4333
4334        plugin_context! {
4335            name: pub MyShareBalance,
4336            context: ShareBalanceContext<T>,
4337            marker: [T],
4338            value: ShareBalanceContext(PhantomData)
4339        }
4340
4341        // ===============================================================================
4342        // `````````````````````` MOCK LAZY-BALANCE VIRTUAL STRUCTS ``````````````````````
4343        // ===============================================================================
4344
4345        /// Mock backing storage for [`LazyBalance::Balance`] using a virtual schema.
4346        ///
4347        /// Fields are accessed via [`VirtualDynField`] and encoded using [`SumDynType`],
4348        /// a convenient default virtual-field representation.
4349        ///
4350        /// Layout (by convention):
4351        /// - `asset[0]` -> effective
4352        /// - `asset[1]` -> issued
4353        /// - `bias[0]`  -> price per share
4354        /// - `time[0]`  -> checkpoint
4355        /// - `time[1]`  -> drainpoint
4356        #[derive(
4357            Clone,
4358            Default,
4359            Debug,
4360            Eq,
4361            PartialEq,
4362            Encode,
4363            Decode,
4364            DecodeWithMemTracking,
4365            TypeInfo,
4366            MaxEncodedLen,
4367        )]
4368        pub struct TestBalance {
4369            /// Asset fields (effective, issued)
4370            pub asset: SumDynType<u128, ConstU32<2>>,
4371
4372            /// Price per share (bias)
4373            pub bias: SumDynType<FixedU128, ConstU32<1>>,
4374
4375            /// Time fields (checkpoint, drainpoint)
4376            pub time: SumDynType<u32, ConstU32<2>>,
4377        }
4378
4379        /// Mock backing storage for [`LazyBalance::SnapShot`] using a virtual schema.
4380        ///
4381        /// Fields are accessed via [`VirtualDynField`] and encoded using [`SumDynType`],
4382        /// a convenient default virtual-field representation.
4383        ///
4384        /// Layout (by convention):
4385        /// - `bias[0]` -> price per share at snapshot
4386        #[derive(
4387            Clone,
4388            Default,
4389            Debug,
4390            Eq,
4391            PartialEq,
4392            Encode,
4393            Decode,
4394            DecodeWithMemTracking,
4395            TypeInfo,
4396            MaxEncodedLen,
4397        )]
4398        pub struct TestSnapshot {
4399            /// Snapshot bias (price per share)
4400            pub bias: SumDynType<FixedU128, ConstU32<1>>,
4401        }
4402
4403        /// Mock backing storage for [`LazyBalance::Receipt`] using a virtual schema.
4404        ///
4405        /// Fields are accessed via [`VirtualDynField`] and encoded using [`SumDynType`],
4406        /// a convenient default virtual-field representation.
4407        ///
4408        /// Layout (by convention):
4409        /// - `asset[0]` -> principal (original deposit)
4410        /// - `asset[1]` -> shares (ownership units)
4411        /// - `bias[0]`  -> deposit-time price per share
4412        /// - `time[0]`  -> checkpoint (time anchor)
4413        #[derive(
4414            Clone,
4415            Default,
4416            Debug,
4417            Eq,
4418            PartialEq,
4419            Encode,
4420            Decode,
4421            DecodeWithMemTracking,
4422            TypeInfo,
4423            MaxEncodedLen,
4424        )]
4425        pub struct TestReceipt {
4426            /// Asset fields (deposit-value, shares)
4427            pub asset: SumDynType<u128, ConstU32<2>>,
4428
4429            /// Deposit-time price per share
4430            pub bias: SumDynType<FixedU128, ConstU32<1>>,
4431
4432            /// Time field (checkpoint)
4433            pub time: SumDynType<u32, ConstU32<1>>,
4434        }
4435
4436        /// Mock backing storage for [`LazyBalance::Limits`] using a virtual schema.
4437        ///
4438        /// Fields are accessed via [`VirtualDynField`] and encoded using [`SumDynType`],
4439        /// a convenient default virtual-field representation.
4440        ///
4441        /// Used with [`Extent`] to express optional bounds.
4442        ///
4443        /// Layout (by convention):
4444        /// - `asset[0]` -> minimum
4445        /// - `asset[1]` -> maximum
4446        /// - `asset[2]` -> optimal
4447        #[derive(
4448            Clone,
4449            Default,
4450            Debug,
4451            Eq,
4452            PartialEq,
4453            Encode,
4454            Decode,
4455            DecodeWithMemTracking,
4456            TypeInfo,
4457            MaxEncodedLen,
4458        )]
4459        pub struct TestLimit {
4460            /// Asset bounds (min, max, optimal)
4461            pub asset: SumDynType<u128, ConstU32<3>>,
4462        }
4463
4464        /// Mock implementation of [`Directive`] for [`LazyBalance::Subject`] execution.
4465        ///
4466        /// Encodes execution preferences:
4467        /// - `precise` -> [`Precision::Exact`] vs [`Precision::BestEffort`]
4468        /// - `force`   -> [`Fortitude::Force`] vs [`Fortitude::Polite`]
4469        ///
4470        /// In [`ShareBalanceFamily`], limits are unbounded, so these flags have no
4471        /// practical effect and are primarily included for interface completeness.
4472        #[derive(
4473            Clone, Eq, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen,
4474        )]
4475        pub struct TestSubject {
4476            /// Precision preference (exact vs best-effort)
4477            pub precise: bool,
4478
4479            /// Execution strictness (force vs polite)
4480            pub force: bool,
4481        }
4482
4483        /// Custom debug output for [`TestSubject`].
4484        ///
4485        /// In [`ShareBalanceFamily`], balance operations are effectively unbounded
4486        /// and do not enforce limits ([`LazyBalance::Limits`]). As a result:
4487        /// - All operations are inherently precise
4488        /// - No forced execution is required
4489        ///
4490        /// Therefore, `precision` and `force` flags are not meaningful here,
4491        /// and are omitted from debug output.
4492        impl std::fmt::Debug for TestSubject {
4493            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4494                write!(f, "-")
4495            }
4496        }
4497
4498        impl Directive for TestSubject {
4499            fn precision(&self) -> Precision {
4500                if self.precise {
4501                    return Precision::Exact;
4502                };
4503                Precision::BestEffort
4504            }
4505
4506            fn fortitude(&self) -> Fortitude {
4507                if self.force {
4508                    return Fortitude::Force;
4509                };
4510                Fortitude::Polite
4511            }
4512
4513            fn new(precision: Precision, fortitude: Fortitude) -> Self {
4514                Self {
4515                    precise: matches!(precision, Precision::Exact),
4516                    force: matches!(fortitude, Fortitude::Force),
4517                }
4518            }
4519        }
4520
4521        impl Default for TestSubject {
4522            fn default() -> Self {
4523                Self {
4524                    precise: false,
4525                    force: false,
4526                }
4527            }
4528        }
4529
4530        // Implements [`VirtualDynField`] for a target type using a [`SumDynType`] field.
4531
4532        macro_rules! impl_v_field {
4533            ($target:ty, $tag:ty, $field:ident, $some:ty, $bound:ty) => {
4534                impl VirtualDynField<$tag> for $target {
4535                    type None = ();
4536                    type Some = $some;
4537                    type Many = Vec<$some>;
4538                    type Repr = SumDynType<$some, $bound>;
4539
4540                    fn access(&self) -> Self::Repr {
4541                        self.$field.clone()
4542                    }
4543
4544                    fn mutate(&mut self, v: Self::Repr) {
4545                        self.$field = v
4546                    }
4547
4548                    fn len(&self) -> usize {
4549                        match &self.$field {
4550                            SumDynType::None => 0,
4551                            SumDynType::Some(_) => 1,
4552                            SumDynType::Many(v) => v.len(),
4553                        }
4554                    }
4555
4556                    fn min(&self) -> usize {
4557                        match &self.$field {
4558                            SumDynType::None => 0,
4559                            SumDynType::Some(_) => 1,
4560                            SumDynType::Many(_) => 0,
4561                        }
4562                    }
4563
4564                    fn max(&self) -> usize {
4565                        match &self.$field {
4566                            SumDynType::None => 0,
4567                            SumDynType::Some(_) => 1,
4568                            SumDynType::Many(_) => <$bound as sp_core::Get<u32>>::get() as usize,
4569                        }
4570                    }
4571                }
4572            };
4573        }
4574
4575        /// Implements an empty [`VirtualDynField`] for a target
4576        /// virtual struct.
4577        ///
4578        /// Field is always `None` with zero capacity (`ConstU32<0>`).
4579        /// All accessors return empty / no-op.
4580        macro_rules! impl_empty_v_field {
4581            ($target:ty, $tag:ty, $some:ty) => {
4582                impl VirtualDynField<$tag> for $target {
4583                    type None = ();
4584                    type Some = $some;
4585                    type Many = Vec<$some>;
4586                    type Repr = SumDynType<$some, ConstU32<0>>;
4587
4588                    fn access(&self) -> Self::Repr {
4589                        SumDynType::None
4590                    }
4591
4592                    fn mutate(&mut self, _: Self::Repr) {}
4593
4594                    fn len(&self) -> usize {
4595                        0
4596                    }
4597                    fn min(&self) -> usize {
4598                        0
4599                    }
4600                    fn max(&self) -> usize {
4601                        0
4602                    }
4603                }
4604            };
4605        }
4606
4607        /// Implements an empty [`VirtualDynExtension`] for a target
4608        /// virtual struct.
4609        ///
4610        /// Returns `Default` on access. Mutation is a no-op (no backing storage).
4611        macro_rules! impl_empty_extension {
4612            ($target:ty, $addon:ty, $provider:ty) => {
4613                impl VirtualDynExtension<$addon> for $target {
4614                    type TypesVia = $provider;
4615
4616                    fn access(
4617                        &self,
4618                    ) -> <Self::TypesVia as VirtualDynExtensionSchema<$addon>>::Repr {
4619                        Default::default()
4620                    }
4621
4622                    fn mutate(
4623                        &mut self,
4624                        _: <Self::TypesVia as VirtualDynExtensionSchema<$addon>>::Repr,
4625                    ) {
4626                    }
4627                }
4628            };
4629        }
4630
4631        impl_v_field!(TestBalance, BalanceAsset, asset, u128, ConstU32<2>);
4632        impl_v_field!(TestBalance, BalanceRational, bias, FixedU128, ConstU32<1>);
4633        impl_v_field!(TestBalance, BalanceTime, time, u32, ConstU32<2>);
4634        impl_empty_extension!(
4635            TestBalance,
4636            BalanceAddon,
4637            ShareBalanceContext<MockShareBalance>
4638        );
4639
4640        impl_v_field!(TestSnapshot, SnapShotRational, bias, FixedU128, ConstU32<1>);
4641        impl_empty_v_field!(TestSnapshot, SnapShotAsset, u128);
4642        impl_empty_v_field!(TestSnapshot, SnapShotTime, u32);
4643        impl_empty_extension!(
4644            TestSnapshot,
4645            SnapShotAddon,
4646            ShareBalanceContext<MockShareBalance>
4647        );
4648
4649        impl_v_field!(TestReceipt, ReceiptAsset, asset, u128, ConstU32<2>);
4650        impl_v_field!(TestReceipt, ReceiptRational, bias, FixedU128, ConstU32<1>);
4651        impl_v_field!(TestReceipt, ReceiptTime, time, u32, ConstU32<1>);
4652        impl_empty_extension!(
4653            TestReceipt,
4654            ReceiptAddon,
4655            ShareBalanceContext<MockShareBalance>
4656        );
4657
4658        impl_v_field!(TestLimit, LimitsAsset, asset, u128, ConstU32<3>);
4659
4660        /// Binds [`LimitsAsset`] for [`Extent`] semantics to a fixed
4661        /// capacity (`ConstU32<3>`) for use with [`VirtualDynField`].
4662        impl VirtualDynBound<LimitsAsset> for TestLimit {
4663            type Bound = ConstU32<3>;
4664        }
4665
4666        /// Implements [`Extent`] for [`TestLimit`] with unbounded semantics.
4667        ///
4668        /// All bounds (`minimum`, `maximum`, `optimal`) return `None`,
4669        /// indicating no constraints on asset values.
4670        impl Extent<LimitsAsset> for TestLimit {
4671            type Scalar = <MockShareBalance as LazyBalance>::Asset;
4672
4673            fn minimum(&self) -> Option<Self::Scalar> {
4674                None
4675            }
4676
4677            fn maximum(&self) -> Option<Self::Scalar> {
4678                None
4679            }
4680
4681            fn optimal(&self) -> Option<Self::Scalar> {
4682                None
4683            }
4684
4685            /// Returns an empty extent with no bounds set
4686            /// (default state, no virtual fields populated).
4687            fn none() -> Self {
4688                Default::default()
4689            }
4690        }
4691
4692        /// Storage prefix for snapshot entries used by [`VirtualNMap`] for [`LazyBalance`]
4693        /// [`virtual`](frame_suite::virtuals) storage bounds.
4694        ///
4695        /// Defined for interface completeness; unused in [`ShareBalanceFamily`].
4696        pub struct SnapshotPrefix;
4697
4698        impl StorageInstance for SnapshotPrefix {
4699            const STORAGE_PREFIX: &'static str = "Snapshots";
4700            fn pallet_prefix() -> &'static str {
4701                "LazyBalance"
4702            }
4703        }
4704
4705        // In-memory snapshot storage for mock `VirtualNMap` implementation.
4706        //
4707        // Key: `(variant, id, time)`
4708        // Value: [`TestSnapshot`]
4709        //
4710        // Used for testing in place of on-chain storage, although unused
4711        // in `ShareBalanceFamily`
4712        thread_local! {
4713            static SNAPSHOTS: RefCell<BTreeMap<(u8,u8,u32), TestSnapshot>> =
4714                RefCell::new(BTreeMap::new());
4715        }
4716
4717        /// Mock [`VirtualNMap`] implementation for snapshot storage although not
4718        /// utilized in [`ShareBalanceFamily`].
4719        ///
4720        /// Uses thread-local in-memory map instead of persistent storage.
4721        /// Provides basic `get`, `insert`, and `remove` operations.
4722        ///
4723        /// Key layout:
4724        /// - `(variant, id, time)` -> snapshot at a given checkpoint
4725        impl VirtualNMap<TestBalance, SnapShotStorage> for MockShareBalance {
4726            type Key = (u8, u8, u32);
4727            type Value = TestSnapshot;
4728
4729            type KeyGen = (
4730                NMapKey<Blake2_128Concat, u8>,
4731                NMapKey<Blake2_128Concat, u8>,
4732                NMapKey<Blake2_128Concat, u32>,
4733            );
4734
4735            type Map = StorageNMap<SnapshotPrefix, Self::KeyGen, TestSnapshot, OptionQuery>;
4736
4737            type Query = Option<TestSnapshot>;
4738
4739            fn get(key: Self::Key) -> Self::Query {
4740                SNAPSHOTS.with(|m| m.borrow().get(&key).cloned())
4741            }
4742
4743            fn insert(key: Self::Key, value: Self::Value) {
4744                SNAPSHOTS.with(|m| m.borrow_mut().insert(key, value));
4745            }
4746
4747            fn remove(key: Self::Key) {
4748                SNAPSHOTS.with(|m| m.borrow_mut().remove(&key));
4749            }
4750        }
4751
4752        /// Helper macro to define [`LazyBalance::Input`] enums where each variant
4753        /// satisfies the blanket [`VirtualCollector`] bounds.
4754        ///
4755        /// For each variant:
4756        /// - [`FromTag`] constructs enum variant from a tuple (actual input)
4757        /// - [`TryIntoTag`] extracts tuple from enum (collector enum)
4758        macro_rules! mock_lazy_input {
4759            (
4760                $name:ident < $lt:lifetime > {
4761                    $(
4762                        $variant:ident (
4763                            $( $field:ident : $ty:ty ),* $(,)?
4764                        )
4765                    ),* $(,)?
4766                }
4767            ) => {
4768
4769                pub enum $name<$lt> {
4770                    $(
4771                        $variant( $( $ty ),* ),
4772                    )*
4773                }
4774
4775                $(
4776                #[allow(unused_parens)]
4777                impl<$lt> FromTag<( $( $ty ),* ), $variant> for $name<$lt> {
4778                    fn from_tag(t: ( $( $ty ),* )) -> Self {
4779                        let ( $( $field ),* ) = t;
4780                        Self::$variant( $( $field ),* )
4781                    }
4782                }
4783
4784                #[allow(unused_parens)]
4785                impl<$lt> TryIntoTag<( $( $ty ),* ), $variant> for $name<$lt> {
4786                    type Error = ();
4787
4788                    fn try_into_tag(self) -> Result<( $( $ty ),* ), Self::Error> {
4789                        match self {
4790                            Self::$variant( $( $field ),* ) => Ok(( $( $field ),* )),
4791                            _ => Err(()),
4792                        }
4793                    }
4794                }
4795                )*
4796            };
4797        }
4798
4799        /// Helper macro to define [`LazyBalance::Output`] enums where each variant
4800        /// satisfies the blanket [`VirtualCollector`] bounds.
4801        ///
4802        /// For each variant:
4803        /// - [`FromTag`] constructs enum variant from a tuple (actual output)
4804        /// - [`TryIntoTag`] extracts tuple from enum (collector enum)
4805        macro_rules! mock_lazy_output {
4806            (
4807                $name:ident < $lt:lifetime > {
4808                    $(
4809                        $variant:ident ( $ty:ty )
4810                    ),* $(,)?
4811                }
4812            ) => {
4813
4814                pub enum $name<$lt> {
4815                    $(
4816                        $variant($ty),
4817                    )*
4818                }
4819
4820                $(
4821                impl<$lt> FromTag<$ty, $variant> for $name<$lt> {
4822                    fn from_tag(t: $ty) -> Self {
4823                        Self::$variant(t)
4824                    }
4825                }
4826
4827                impl<$lt> TryIntoTag<$ty, $variant> for $name<$lt> {
4828                    type Error = ();
4829
4830                    fn try_into_tag(self) -> Result<$ty, Self::Error> {
4831                        match self {
4832                            Self::$variant(v) => Ok(v),
4833                            _ => Err(()),
4834                        }
4835                    }
4836                }
4837                )*
4838            };
4839        }
4840
4841        mock_lazy_input!(
4842            TestInput<'a> {
4843
4844                Deposit(
4845                    balance: MutHandle<'a, TestBalance>,
4846                    variant: Cow<'a, u8>,
4847                    id: Cow<'a, u8>,
4848                    asset: Cow<'a, u128>,
4849                    subject: Cow<'a, TestSubject>,
4850                ),
4851
4852                Mint(
4853                    balance: MutHandle<'a, TestBalance>,
4854                    variant: Cow<'a, u8>,
4855                    id: Cow<'a, u8>,
4856                    asset: Cow<'a, u128>,
4857                    subject: Cow<'a, TestSubject>,
4858                ),
4859
4860                Reap(
4861                    balance: MutHandle<'a, TestBalance>,
4862                    variant: Cow<'a, u8>,
4863                    id: Cow<'a, u8>,
4864                    asset: Cow<'a, u128>,
4865                    subject: Cow<'a, TestSubject>,
4866                ),
4867
4868                Withdraw(
4869                    balance: MutHandle<'a, TestBalance>,
4870                    variant: Cow<'a, u8>,
4871                    id: Cow<'a, u8>,
4872                    receipt: Cow<'a, TestReceipt>,
4873                ),
4874
4875                Drain(
4876                    balance: MutHandle<'a, TestBalance>,
4877                    variant: Cow<'a, u8>,
4878                    id: Cow<'a, u8>,
4879                ),
4880
4881                CanDeposit(
4882                    balance: Cow<'a, TestBalance>,
4883                    variant: Cow<'a, u8>,
4884                    id: Cow<'a, u8>,
4885                    asset: Cow<'a, u128>,
4886                    subject: Cow<'a, TestSubject>,
4887                ),
4888
4889                CanMint(
4890                    balance: Cow<'a, TestBalance>,
4891                    variant: Cow<'a, u8>,
4892                    id: Cow<'a, u8>,
4893                    asset: Cow<'a, u128>,
4894                    subject: Cow<'a, TestSubject>,
4895                ),
4896
4897                CanReap(
4898                    balance: Cow<'a, TestBalance>,
4899                    variant: Cow<'a, u8>,
4900                    id: Cow<'a, u8>,
4901                    asset: Cow<'a, u128>,
4902                    subject: Cow<'a, TestSubject>,
4903                ),
4904
4905                CanWithdraw(
4906                    balance: Cow<'a, TestBalance>,
4907                    variant: Cow<'a, u8>,
4908                    id: Cow<'a, u8>,
4909                    receipt: Cow<'a, TestReceipt>,
4910                ),
4911
4912                TotalValue(
4913                    balance: Cow<'a, TestBalance>,
4914                    variant: Cow<'a, u8>,
4915                    id: Cow<'a, u8>,
4916                ),
4917
4918                ReceiptActiveValue(
4919                    balance: Cow<'a, TestBalance>,
4920                    variant: Cow<'a, u8>,
4921                    id: Cow<'a, u8>,
4922                    receipt: Cow<'a, TestReceipt>,
4923                ),
4924
4925                HasDeposits(
4926                    balance: Cow<'a, TestBalance>,
4927                    variant: Cow<'a, u8>,
4928                    id: Cow<'a, u8>,
4929                ),
4930
4931                ReceiptDepositValue(
4932                    receipt: Cow<'a, TestReceipt>,
4933                ),
4934
4935                DepositLimits (
4936                    balance: Cow<'a, TestBalance>,
4937                    variant: Cow<'a, u8>,
4938                    id: Cow<'a, u8>,
4939                    subject: Cow<'a, TestSubject>,
4940                ),
4941
4942                MintLimits (
4943                    balance: Cow<'a, TestBalance>,
4944                    variant: Cow<'a, u8>,
4945                    id: Cow<'a, u8>,
4946                    subject: Cow<'a, TestSubject>,
4947                ),
4948
4949                ReapLimits (
4950                    balance: Cow<'a, TestBalance>,
4951                    variant: Cow<'a, u8>,
4952                    id: Cow<'a, u8>,
4953                    subject: Cow<'a, TestSubject>,
4954                ),
4955            }
4956        );
4957
4958        mock_lazy_output!(
4959            TestOutput<'a> {
4960
4961                Deposit(Result<(Cow<'a, u128>, Cow<'a, TestReceipt>), ShareBalanceError>),
4962
4963                Mint(Result<Cow<'a, u128>, ShareBalanceError>),
4964
4965                Reap(Result<Cow<'a, u128>, ShareBalanceError>),
4966
4967                Withdraw(Result<Cow<'a, u128>, ShareBalanceError>),
4968
4969                Drain(Result<Cow<'a, u128>, ShareBalanceError>),
4970
4971                CanDeposit(Result<(), ShareBalanceError>),
4972
4973                CanMint(Result<(), ShareBalanceError>),
4974
4975                CanReap(Result<(), ShareBalanceError>),
4976
4977                CanWithdraw(Result<(), ShareBalanceError>),
4978
4979                TotalValue(Result<Cow<'a, u128>, ShareBalanceError>),
4980
4981                ReceiptActiveValue(Result<Cow<'a, u128>, ShareBalanceError>),
4982
4983                HasDeposits(Result<(), ShareBalanceError>),
4984
4985                ReceiptDepositValue(Result<Cow<'a, u128>, ShareBalanceError>),
4986
4987                DepositLimits(Result<Cow<'a, TestLimit>, ShareBalanceError>),
4988
4989                MintLimits(Result<Cow<'a, TestLimit>, ShareBalanceError>),
4990
4991                ReapLimits(Result<Cow<'a, TestLimit>, ShareBalanceError>),
4992            }
4993        );
4994    }
4995}