radix_engine/system/system_modules/costing/
fee_reserve.rs

1use super::FeeReserveFinalizationSummary;
2use crate::internal_prelude::*;
3use crate::{
4    errors::CanBeAbortion,
5    transaction::{AbortReason, CostingParameters},
6};
7use radix_engine_interface::blueprints::resource::LiquidFungibleResource;
8use radix_transactions::model::TipSpecifier;
9use radix_transactions::prelude::TransactionCostingParameters;
10use sbor::rust::cmp::min;
11
12#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)]
13pub enum FeeReserveError {
14    InsufficientBalance {
15        required: Decimal,
16        remaining: Decimal,
17    },
18    Overflow,
19    LimitExceeded {
20        limit: u32,
21        committed: u32,
22        new: u32,
23    },
24    LoanRepaymentFailed {
25        xrd_owed: Decimal,
26    },
27    Abort(AbortReason),
28}
29
30#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, ScryptoSbor)]
31pub enum StorageType {
32    State,
33    Archive,
34}
35
36impl CanBeAbortion for FeeReserveError {
37    fn abortion(&self) -> Option<&AbortReason> {
38        match self {
39            Self::Abort(reason) => Some(reason),
40            _ => None,
41        }
42    }
43}
44
45/// This is only allowed before a transaction properly begins.
46/// After any other methods are called, this cannot be called again.
47
48pub trait PreExecutionFeeReserve {
49    fn consume_deferred_execution(&mut self, cost_units: u32) -> Result<(), FeeReserveError>;
50
51    fn consume_deferred_finalization(&mut self, cost_units: u32) -> Result<(), FeeReserveError>;
52
53    fn consume_deferred_storage(
54        &mut self,
55        storage_type: StorageType,
56        size_increase: usize,
57    ) -> Result<(), FeeReserveError>;
58}
59
60pub trait ExecutionFeeReserve {
61    fn consume_execution(&mut self, cost_units: u32) -> Result<(), FeeReserveError>;
62
63    fn consume_finalization(&mut self, cost_units: u32) -> Result<(), FeeReserveError>;
64
65    fn consume_storage(
66        &mut self,
67        storage_type: StorageType,
68        size_increase: usize,
69    ) -> Result<(), FeeReserveError>;
70
71    fn consume_royalty(
72        &mut self,
73        royalty_amount: RoyaltyAmount,
74        recipient: RoyaltyRecipient,
75    ) -> Result<(), FeeReserveError>;
76
77    fn lock_fee(&mut self, vault_id: NodeId, fee: LiquidFungibleResource, contingent: bool);
78}
79
80pub trait FinalizingFeeReserve {
81    fn finalize(
82        self,
83    ) -> (
84        FeeReserveFinalizationSummary,
85        CostingParameters,
86        TransactionCostingParameters,
87    );
88}
89
90pub trait FeeReserve: PreExecutionFeeReserve + ExecutionFeeReserve + FinalizingFeeReserve {}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, ScryptoSbor)]
93pub enum RoyaltyRecipient {
94    Package(PackageAddress, NodeId),
95    Component(ComponentAddress, NodeId),
96}
97
98impl RoyaltyRecipient {
99    pub fn vault_id(&self) -> NodeId {
100        match self {
101            RoyaltyRecipient::Package(_, v) | RoyaltyRecipient::Component(_, v) => *v,
102        }
103    }
104}
105
106#[derive(Debug, Clone, ScryptoSbor)]
107pub struct SystemLoanFeeReserve {
108    costing_parameters: CostingParameters,
109    transaction_costing_parameters: TransactionCostingParameters,
110    abort_when_loan_repaid: bool,
111
112    /// (Cache) The effective execution cost unit price, with tips considered
113    effective_execution_cost_unit_price: Decimal,
114    /// (Cache) The effective finalization cost unit price, with tips considered
115    effective_finalization_cost_unit_price: Decimal,
116
117    /// The XRD balance
118    xrd_balance: Decimal,
119    /// The amount of XRD owed to the system
120    xrd_owed: Decimal,
121
122    /// Execution costs
123    execution_cost_units_committed: u32,
124    execution_cost_units_deferred: u32,
125
126    // Finalization costs
127    finalization_cost_units_committed: u32,
128    finalization_cost_units_deferred: u32,
129
130    /// Royalty costs
131    royalty_cost_committed: Decimal,
132    royalty_cost_breakdown: IndexMap<RoyaltyRecipient, Decimal>,
133
134    /// Storage Costs
135    storage_cost_committed: Decimal,
136    storage_cost_deferred: IndexMap<StorageType, usize>,
137
138    /// Payments made during the execution of a transaction.
139    locked_fees: Vec<(NodeId, LiquidFungibleResource, bool)>,
140}
141
142impl Default for SystemLoanFeeReserve {
143    fn default() -> Self {
144        Self::new(
145            CostingParameters::babylon_genesis(),
146            TransactionCostingParameters::default(),
147            false,
148        )
149    }
150}
151
152#[inline]
153fn checked_add(a: u32, b: u32) -> Result<u32, FeeReserveError> {
154    a.checked_add(b).ok_or(FeeReserveError::Overflow)
155}
156
157#[inline]
158fn checked_add_assign(value: &mut u32, summand: u32) -> Result<(), FeeReserveError> {
159    *value = checked_add(*value, summand)?;
160    Ok(())
161}
162
163impl SystemLoanFeeReserve {
164    pub fn new(
165        costing_parameters: CostingParameters,
166        transaction_costing_parameters: TransactionCostingParameters,
167        abort_when_loan_repaid: bool,
168    ) -> Self {
169        // Sanity checks
170        assert!(!costing_parameters.execution_cost_unit_price.is_negative());
171        assert!(!costing_parameters
172            .finalization_cost_unit_price
173            .is_negative());
174        assert!(!costing_parameters.usd_price.is_negative());
175        assert!(!costing_parameters.state_storage_price.is_negative());
176        assert!(!costing_parameters.archive_storage_price.is_negative());
177        assert!(!transaction_costing_parameters
178            .free_credit_in_xrd
179            .is_negative());
180
181        let tip_multiplier = transaction_costing_parameters.tip.fee_multiplier();
182
183        let effective_execution_cost_unit_price = costing_parameters
184            .execution_cost_unit_price
185            .checked_mul(tip_multiplier)
186            .unwrap();
187
188        let effective_finalization_cost_unit_price = costing_parameters
189            .finalization_cost_unit_price
190            .checked_mul(tip_multiplier)
191            .unwrap();
192
193        let system_loan_in_xrd = effective_execution_cost_unit_price
194            .checked_mul(costing_parameters.execution_cost_unit_loan)
195            .unwrap();
196
197        let starting_xrd_balance = system_loan_in_xrd
198            .checked_add(transaction_costing_parameters.free_credit_in_xrd)
199            .expect("Invalid system loan or free credit amount");
200
201        Self {
202            costing_parameters,
203            transaction_costing_parameters,
204            abort_when_loan_repaid,
205
206            // Cache
207            effective_execution_cost_unit_price,
208            effective_finalization_cost_unit_price,
209
210            // Running balance
211            xrd_balance: starting_xrd_balance,
212            xrd_owed: system_loan_in_xrd,
213
214            // Internal states
215            execution_cost_units_committed: 0,
216            execution_cost_units_deferred: 0,
217
218            finalization_cost_units_committed: 0,
219            finalization_cost_units_deferred: 0,
220
221            royalty_cost_breakdown: index_map_new(),
222            royalty_cost_committed: Decimal::ZERO,
223
224            storage_cost_committed: Decimal::ZERO,
225            storage_cost_deferred: index_map_new(),
226
227            locked_fees: Vec::new(),
228        }
229    }
230
231    pub fn costing_parameters(&self) -> &CostingParameters {
232        &self.costing_parameters
233    }
234
235    pub fn transaction_costing_parameters(&self) -> &TransactionCostingParameters {
236        &self.transaction_costing_parameters
237    }
238
239    pub fn execution_cost_unit_limit(&self) -> u32 {
240        self.costing_parameters.execution_cost_unit_limit
241    }
242
243    pub fn execution_cost_unit_price(&self) -> Decimal {
244        self.costing_parameters.execution_cost_unit_price
245    }
246
247    pub fn finalization_cost_unit_limit(&self) -> u32 {
248        self.costing_parameters.finalization_cost_unit_limit
249    }
250
251    pub fn finalization_cost_unit_price(&self) -> Decimal {
252        self.costing_parameters.finalization_cost_unit_price
253    }
254
255    pub fn usd_price(&self) -> Decimal {
256        self.costing_parameters.usd_price
257    }
258
259    pub fn tip(&self) -> TipSpecifier {
260        self.transaction_costing_parameters.tip
261    }
262
263    pub fn fee_balance(&self) -> Decimal {
264        self.xrd_balance
265    }
266
267    pub fn royalty_cost_breakdown(&self) -> &IndexMap<RoyaltyRecipient, Decimal> {
268        &self.royalty_cost_breakdown
269    }
270
271    fn check_execution_cost_unit_limit(&self, cost_units: u32) -> Result<(), FeeReserveError> {
272        if checked_add(self.execution_cost_units_committed, cost_units)?
273            > self.costing_parameters.execution_cost_unit_limit
274        {
275            return Err(FeeReserveError::LimitExceeded {
276                limit: self.costing_parameters.execution_cost_unit_limit,
277                committed: self.execution_cost_units_committed,
278                new: cost_units,
279            });
280        }
281        Ok(())
282    }
283
284    fn check_finalization_cost_unit_limit(&self, cost_units: u32) -> Result<(), FeeReserveError> {
285        if checked_add(self.finalization_cost_units_committed, cost_units)?
286            > self.costing_parameters.finalization_cost_unit_limit
287        {
288            return Err(FeeReserveError::LimitExceeded {
289                limit: self.costing_parameters.finalization_cost_unit_limit,
290                committed: self.finalization_cost_units_committed,
291                new: cost_units,
292            });
293        }
294        Ok(())
295    }
296
297    fn consume_execution_internal(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
298        self.check_execution_cost_unit_limit(cost_units)?;
299
300        let amount = self
301            .effective_execution_cost_unit_price
302            .checked_mul(cost_units)
303            .ok_or(FeeReserveError::Overflow)?;
304        if self.xrd_balance < amount {
305            return Err(FeeReserveError::InsufficientBalance {
306                required: amount,
307                remaining: self.xrd_balance,
308            });
309        } else {
310            self.xrd_balance -= amount;
311            self.execution_cost_units_committed += cost_units;
312            Ok(())
313        }
314    }
315
316    fn consume_finalization_internal(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
317        self.check_finalization_cost_unit_limit(cost_units)?;
318
319        let amount = self
320            .effective_finalization_cost_unit_price
321            .checked_mul(cost_units)
322            .ok_or(FeeReserveError::Overflow)?;
323        if self.xrd_balance < amount {
324            return Err(FeeReserveError::InsufficientBalance {
325                required: amount,
326                remaining: self.xrd_balance,
327            });
328        } else {
329            self.xrd_balance -= amount;
330            self.finalization_cost_units_committed += cost_units;
331            Ok(())
332        }
333    }
334
335    fn consume_royalty_internal(
336        &mut self,
337        royalty_amount: RoyaltyAmount,
338        recipient: RoyaltyRecipient,
339    ) -> Result<(), FeeReserveError> {
340        let amount = match royalty_amount {
341            RoyaltyAmount::Xrd(xrd_amount) => xrd_amount,
342            RoyaltyAmount::Usd(usd_amount) => usd_amount
343                .checked_mul(self.costing_parameters.usd_price)
344                .ok_or(FeeReserveError::Overflow)?,
345            RoyaltyAmount::Free => Decimal::ZERO,
346        };
347
348        if self.xrd_balance < amount {
349            return Err(FeeReserveError::InsufficientBalance {
350                required: amount,
351                remaining: self.xrd_balance,
352            });
353        } else {
354            self.xrd_balance -= amount;
355            self.royalty_cost_breakdown
356                .entry(recipient)
357                .or_default()
358                .add_assign(amount);
359            self.royalty_cost_committed += amount;
360            Ok(())
361        }
362    }
363
364    pub fn repay_all(&mut self) -> Result<(), FeeReserveError> {
365        // Apply deferred execution cost
366        self.consume_execution_internal(self.execution_cost_units_deferred)?;
367        self.execution_cost_units_deferred = 0;
368
369        // Apply deferred finalization cost
370        self.consume_finalization_internal(self.finalization_cost_units_deferred)?;
371        self.finalization_cost_units_deferred = 0;
372
373        // Apply deferred storage cost
374        let types: Vec<StorageType> = self.storage_cost_deferred.keys().cloned().collect();
375        for t in types {
376            self.consume_storage(t, self.storage_cost_deferred.get(&t).cloned().unwrap())?;
377            self.storage_cost_deferred.swap_remove(&t);
378        }
379
380        // Repay owed with balance
381        let amount = min(self.xrd_balance, self.xrd_owed);
382        self.xrd_owed -= amount;
383        self.xrd_balance -= amount; // not used afterwards
384
385        // Check outstanding loan
386        if !self.xrd_owed.is_zero() {
387            return Err(FeeReserveError::LoanRepaymentFailed {
388                xrd_owed: self.xrd_owed,
389            });
390        }
391
392        if self.abort_when_loan_repaid {
393            return Err(FeeReserveError::Abort(
394                AbortReason::ConfiguredAbortTriggeredOnFeeLoanRepayment,
395            ));
396        }
397
398        Ok(())
399    }
400
401    pub fn revert_royalty(&mut self) {
402        self.xrd_balance += self.royalty_cost_committed;
403        self.royalty_cost_breakdown.clear();
404        self.royalty_cost_committed = Decimal::ZERO;
405    }
406
407    #[inline]
408    pub fn fully_repaid(&self) -> bool {
409        // The xrd_owed state is not reset before all deferred costs are applied.
410        // Thus, not checking the deferred balance
411        self.xrd_owed == Decimal::ZERO
412    }
413}
414
415impl PreExecutionFeeReserve for SystemLoanFeeReserve {
416    fn consume_deferred_execution(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
417        checked_add_assign(&mut self.execution_cost_units_deferred, cost_units)?;
418
419        Ok(())
420    }
421
422    fn consume_deferred_finalization(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
423        checked_add_assign(&mut self.finalization_cost_units_deferred, cost_units)?;
424
425        Ok(())
426    }
427
428    fn consume_deferred_storage(
429        &mut self,
430        storage_type: StorageType,
431        size_increase: usize,
432    ) -> Result<(), FeeReserveError> {
433        self.storage_cost_deferred
434            .entry(storage_type)
435            .or_default()
436            .add_assign(size_increase);
437
438        Ok(())
439    }
440}
441
442impl ExecutionFeeReserve for SystemLoanFeeReserve {
443    fn consume_execution(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
444        if cost_units == 0 {
445            return Ok(());
446        }
447
448        self.consume_execution_internal(cost_units)?;
449
450        if !self.fully_repaid()
451            && self.execution_cost_units_committed
452                >= self.costing_parameters.execution_cost_unit_loan
453        {
454            self.repay_all()?;
455        }
456
457        Ok(())
458    }
459
460    fn consume_finalization(&mut self, cost_units: u32) -> Result<(), FeeReserveError> {
461        if cost_units == 0 {
462            return Ok(());
463        }
464
465        self.consume_finalization_internal(cost_units)?;
466
467        Ok(())
468    }
469
470    fn consume_royalty(
471        &mut self,
472        royalty_amount: RoyaltyAmount,
473        recipient: RoyaltyRecipient,
474    ) -> Result<(), FeeReserveError> {
475        if royalty_amount.is_zero() {
476            return Ok(());
477        }
478        if royalty_amount.is_negative() {
479            panic!("System invariant broken: Encountered negative royalty amount")
480        }
481
482        self.consume_royalty_internal(royalty_amount, recipient)?;
483
484        Ok(())
485    }
486
487    fn consume_storage(
488        &mut self,
489        storage_type: StorageType,
490        size_increase: usize,
491    ) -> Result<(), FeeReserveError> {
492        let amount = match storage_type {
493            StorageType::State => self.costing_parameters.state_storage_price,
494            StorageType::Archive => self.costing_parameters.archive_storage_price,
495        }
496        .checked_mul(size_increase)
497        .ok_or(FeeReserveError::Overflow)?;
498
499        if self.xrd_balance < amount {
500            return Err(FeeReserveError::InsufficientBalance {
501                required: amount,
502                remaining: self.xrd_balance,
503            });
504        } else {
505            self.xrd_balance -= amount;
506            self.storage_cost_committed += amount;
507            Ok(())
508        }
509    }
510
511    fn lock_fee(&mut self, vault_id: NodeId, mut fee: LiquidFungibleResource, contingent: bool) {
512        // Update balance
513        if !contingent {
514            self.xrd_balance = self
515                .xrd_balance
516                .checked_add(fee.amount())
517                .expect("No overflow due to limited XRD supply");
518        }
519
520        // Move resource
521        self.locked_fees
522            .push((vault_id, fee.take_all(), contingent));
523    }
524}
525
526impl FinalizingFeeReserve for SystemLoanFeeReserve {
527    fn finalize(
528        self,
529    ) -> (
530        FeeReserveFinalizationSummary,
531        CostingParameters,
532        TransactionCostingParameters,
533    ) {
534        let total_execution_cost_in_xrd = self
535            .costing_parameters
536            .execution_cost_unit_price
537            .checked_mul(Decimal::from(self.execution_cost_units_committed))
538            .unwrap();
539
540        let total_finalization_cost_in_xrd = self
541            .costing_parameters
542            .finalization_cost_unit_price
543            .checked_mul(Decimal::from(self.finalization_cost_units_committed))
544            .unwrap();
545
546        let tip_proportion = self.transaction_costing_parameters.tip.proportion();
547        let total_tipping_cost_in_xrd = total_execution_cost_in_xrd
548            .checked_mul(tip_proportion)
549            .unwrap()
550            .checked_add(
551                total_finalization_cost_in_xrd
552                    .checked_mul(tip_proportion)
553                    .unwrap(),
554            )
555            .unwrap();
556
557        let summary = FeeReserveFinalizationSummary {
558            total_execution_cost_units_consumed: self.execution_cost_units_committed,
559            total_finalization_cost_units_consumed: self.finalization_cost_units_committed,
560
561            total_execution_cost_in_xrd,
562            total_finalization_cost_in_xrd,
563            total_tipping_cost_in_xrd,
564            total_royalty_cost_in_xrd: self.royalty_cost_committed,
565            total_storage_cost_in_xrd: self.storage_cost_committed,
566            total_bad_debt_in_xrd: self.xrd_owed,
567            locked_fees: self.locked_fees,
568            royalty_cost_breakdown: self.royalty_cost_breakdown,
569        };
570
571        (
572            summary,
573            self.costing_parameters,
574            self.transaction_costing_parameters,
575        )
576    }
577}
578
579impl FeeReserve for SystemLoanFeeReserve {}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    const TEST_COMPONENT: ComponentAddress =
586        component_address(EntityType::GlobalGenericComponent, 5);
587    const TEST_VAULT_ID: NodeId = NodeId([0u8; NodeId::LENGTH]);
588    const TEST_VAULT_ID_2: NodeId = NodeId([1u8; NodeId::LENGTH]);
589
590    fn xrd<T: Into<Decimal>>(amount: T) -> LiquidFungibleResource {
591        LiquidFungibleResource::new(amount.into())
592    }
593
594    fn create_test_fee_reserve(
595        execution_cost_unit_price: Decimal,
596        usd_price: Decimal,
597        state_storage_price: Decimal,
598        tip_percentage: u16,
599        execution_cost_unit_limit: u32,
600        execution_cost_unit_loan: u32,
601        abort_when_loan_repaid: bool,
602    ) -> SystemLoanFeeReserve {
603        let mut costing_parameters = CostingParameters::babylon_genesis();
604        costing_parameters.execution_cost_unit_price = execution_cost_unit_price;
605        costing_parameters.execution_cost_unit_limit = execution_cost_unit_limit;
606        costing_parameters.execution_cost_unit_loan = execution_cost_unit_loan;
607        costing_parameters.usd_price = usd_price;
608        costing_parameters.state_storage_price = state_storage_price;
609        let mut transaction_costing_parameters = TransactionCostingParameters::default();
610        transaction_costing_parameters.tip = TipSpecifier::Percentage(tip_percentage);
611
612        SystemLoanFeeReserve::new(
613            costing_parameters,
614            transaction_costing_parameters,
615            abort_when_loan_repaid,
616        )
617    }
618
619    #[test]
620    fn test_consume_and_repay() {
621        let mut fee_reserve = create_test_fee_reserve(dec!(1), dec!(1), dec!(0), 2, 100, 5, false);
622        fee_reserve.consume_execution(2).unwrap();
623        fee_reserve.lock_fee(TEST_VAULT_ID, xrd(3), false);
624        fee_reserve.repay_all().unwrap();
625        let (summary, _, _) = fee_reserve.finalize();
626        assert_eq!(summary.loan_fully_repaid(), true);
627        assert_eq!(summary.total_execution_cost_units_consumed, 2);
628        assert_eq!(summary.total_execution_cost_in_xrd, dec!("2"));
629        assert_eq!(summary.total_tipping_cost_in_xrd, dec!("0.04"));
630        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("0"));
631        assert_eq!(summary.total_bad_debt_in_xrd, dec!("0"));
632    }
633
634    #[test]
635    fn test_out_of_cost_unit() {
636        let mut fee_reserve = create_test_fee_reserve(dec!(1), dec!(1), dec!(0), 2, 100, 5, false);
637        assert_eq!(
638            fee_reserve.consume_execution(6),
639            Err(FeeReserveError::InsufficientBalance {
640                required: dec!("6.12"),
641                remaining: dec!("5.1"),
642            }),
643        );
644        fee_reserve.repay_all().unwrap();
645        let (summary, _, _) = fee_reserve.finalize();
646        assert_eq!(summary.loan_fully_repaid(), true);
647        assert_eq!(summary.total_execution_cost_units_consumed, 0);
648        assert_eq!(summary.total_execution_cost_in_xrd, dec!("0"));
649        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("0"));
650        assert_eq!(summary.total_bad_debt_in_xrd, dec!("0"));
651    }
652
653    #[test]
654    fn test_lock_fee() {
655        let mut fee_reserve =
656            create_test_fee_reserve(dec!(1), dec!(1), dec!(0), 2, 100, 500, false);
657        fee_reserve.lock_fee(TEST_VAULT_ID, xrd(100), false);
658        fee_reserve.repay_all().unwrap();
659        let (summary, _, _) = fee_reserve.finalize();
660        assert_eq!(summary.loan_fully_repaid(), true);
661        assert_eq!(summary.total_execution_cost_units_consumed, 0);
662        assert_eq!(summary.total_execution_cost_in_xrd, dec!("0"));
663        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("0"));
664        assert_eq!(summary.total_bad_debt_in_xrd, dec!("0"));
665    }
666
667    #[test]
668    fn test_xrd_cost_unit_conversion() {
669        let mut fee_reserve =
670            create_test_fee_reserve(dec!(5), dec!(1), dec!(0), 0, 100, 500, false);
671        fee_reserve.lock_fee(TEST_VAULT_ID, xrd(100), false);
672        fee_reserve.repay_all().unwrap();
673        let (summary, _, _) = fee_reserve.finalize();
674        assert_eq!(summary.loan_fully_repaid(), true);
675        assert_eq!(summary.total_execution_cost_units_consumed, 0);
676        assert_eq!(summary.total_execution_cost_in_xrd, dec!("0"));
677        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("0"));
678        assert_eq!(summary.total_bad_debt_in_xrd, dec!("0"));
679        assert_eq!(summary.locked_fees, vec![(TEST_VAULT_ID, xrd(100), false)],);
680    }
681
682    #[test]
683    fn test_bad_debt() {
684        let mut fee_reserve = create_test_fee_reserve(dec!(5), dec!(1), dec!(0), 1, 100, 50, false);
685        fee_reserve.consume_execution(2).unwrap();
686        assert_eq!(
687            fee_reserve.repay_all(),
688            Err(FeeReserveError::LoanRepaymentFailed {
689                xrd_owed: dec!("10.1")
690            })
691        );
692        let (summary, _, _) = fee_reserve.finalize();
693        assert_eq!(summary.loan_fully_repaid(), false);
694        assert_eq!(summary.total_execution_cost_units_consumed, 2);
695        assert_eq!(summary.total_execution_cost_in_xrd, dec!("10"));
696        assert_eq!(summary.total_tipping_cost_in_xrd, dec!("0.1"));
697        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("0"));
698        assert_eq!(summary.total_bad_debt_in_xrd, dec!("10.1"));
699        assert_eq!(summary.locked_fees, vec![],);
700    }
701
702    #[test]
703    fn test_royalty_execution_mix() {
704        let mut fee_reserve = create_test_fee_reserve(dec!(5), dec!(2), dec!(0), 1, 100, 50, false);
705        fee_reserve.consume_execution(2).unwrap();
706        fee_reserve
707            .consume_royalty(
708                RoyaltyAmount::Xrd(2.into()),
709                RoyaltyRecipient::Package(PACKAGE_PACKAGE, TEST_VAULT_ID),
710            )
711            .unwrap();
712        fee_reserve
713            .consume_royalty(
714                RoyaltyAmount::Usd(7.into()),
715                RoyaltyRecipient::Package(PACKAGE_PACKAGE, TEST_VAULT_ID),
716            )
717            .unwrap();
718        fee_reserve.lock_fee(TEST_VAULT_ID, xrd(100), false);
719        fee_reserve.repay_all().unwrap();
720        let (summary, _, _) = fee_reserve.finalize();
721        assert_eq!(summary.loan_fully_repaid(), true);
722        assert_eq!(summary.total_execution_cost_in_xrd, dec!("10"));
723        assert_eq!(summary.total_tipping_cost_in_xrd, dec!("0.1"));
724        assert_eq!(summary.total_royalty_cost_in_xrd, dec!("16"));
725        assert_eq!(summary.total_bad_debt_in_xrd, dec!("0"));
726        assert_eq!(summary.locked_fees, vec![(TEST_VAULT_ID, xrd(100), false)]);
727        assert_eq!(summary.total_execution_cost_units_consumed, 2);
728        assert_eq!(
729            summary.royalty_cost_breakdown,
730            indexmap!(
731                RoyaltyRecipient::Package(PACKAGE_PACKAGE, TEST_VAULT_ID) => dec!("16")
732            )
733        );
734    }
735
736    #[test]
737    fn test_royalty_insufficient_balance() {
738        let mut fee_reserve =
739            create_test_fee_reserve(dec!(1), dec!(1), dec!(0), 0, 1000, 50, false);
740        fee_reserve.lock_fee(TEST_VAULT_ID, xrd(100), false);
741        fee_reserve
742            .consume_royalty(
743                RoyaltyAmount::Xrd(90.into()),
744                RoyaltyRecipient::Package(PACKAGE_PACKAGE, TEST_VAULT_ID),
745            )
746            .unwrap();
747        assert_eq!(
748            fee_reserve.consume_royalty(
749                RoyaltyAmount::Xrd(80.into()),
750                RoyaltyRecipient::Component(TEST_COMPONENT, TEST_VAULT_ID_2),
751            ),
752            Err(FeeReserveError::InsufficientBalance {
753                required: dec!("80"),
754                remaining: dec!("60"),
755            }),
756        );
757    }
758}