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}