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