Skip to main content

solana_runtime/bank/
fee_distribution.rs

1use {
2    super::Bank,
3    crate::{bank::CollectorFeeDetails, reward_info::RewardInfo},
4    agave_reserved_account_keys::ReservedAccountKeys,
5    log::debug,
6    solana_account::{AccountSharedData, ReadableAccount, WritableAccount},
7    solana_fee::FeeFeatures,
8    solana_pubkey::Pubkey,
9    solana_rent::Rent,
10    solana_reward_info::RewardType,
11    solana_runtime_transaction::{
12        transaction_meta::TransactionConfiguration, transaction_with_meta::TransactionWithMeta,
13    },
14    solana_sdk_ids::incinerator,
15    solana_svm::rent_calculator::check_static_account_rent_state_transition,
16    solana_system_interface::program as system_program,
17    std::{result::Result, sync::atomic::Ordering::Relaxed},
18    thiserror::Error,
19};
20
21#[derive(Error, Debug, PartialEq)]
22pub(super) enum DepositFeeError {
23    #[error("fee account became rent paying")]
24    InvalidRentPayingAccount,
25    #[error("lamport overflow")]
26    LamportOverflow,
27    #[error("invalid fee account owner")]
28    InvalidAccountOwner,
29    #[error("collector is a reserved account")]
30    ReservedCollector,
31}
32
33/// Helper enum used to distinguish external collector types allowed by
34/// SIMD-0232.
35///
36/// The term "external" is used to exclude the vote account itself, which is a
37/// valid collector.
38pub(super) enum ExternalCollectorType {
39    /// A rent-exempt, non-incinerator, non-reserved account owned by the system
40    /// program
41    SystemAccount,
42    /// Specifically, the incinerator account, denoted by `incinerator::id()`
43    Incinerator,
44}
45
46#[derive(Default)]
47pub struct FeeDistribution {
48    deposit: u64,
49    burn: u64,
50}
51
52impl FeeDistribution {
53    pub fn get_deposit(&self) -> u64 {
54        self.deposit
55    }
56}
57
58impl Bank {
59    // Distribute collected transaction fees for this slot to the block revenue collector
60    // id for the current leader.
61    //
62    // Each validator is incentivized to process more transactions to earn more transaction fees.
63    // Transaction fees are rewarded for the computing resource utilization cost, directly
64    // proportional to their actual processing power.
65    //
66    // The leader is rotated according to stake-weighted leader schedule. So the opportunity of
67    // earning transaction fees are fairly distributed by stake. And missing the opportunity
68    // (not producing a block as a leader) earns nothing. So, being online is incentivized as a
69    // form of transaction fees as well.
70    pub(super) fn distribute_transaction_fee_details(&self) {
71        let fee_details = self.collector_fee_details.read().unwrap();
72
73        let FeeDistribution { deposit, burn } =
74            self.calculate_reward_and_burn_fee_details(&fee_details);
75
76        let total_burn = self.deposit_or_burn_fee(deposit).saturating_add(burn);
77        self.capitalization.fetch_sub(total_burn, Relaxed);
78    }
79
80    pub fn calculate_reward_for_transaction(
81        &self,
82        transaction: &impl TransactionWithMeta,
83        transaction_configuration: &TransactionConfiguration,
84    ) -> u64 {
85        let fee_details = solana_fee::calculate_fee_details(
86            transaction,
87            self.fee_structure().lamports_per_signature,
88            transaction_configuration.priority_fee_lamports,
89            FeeFeatures::from(self.feature_set.as_ref()),
90        );
91        let FeeDistribution {
92            deposit: reward,
93            burn: _,
94        } = self.calculate_reward_and_burn_fee_details(&CollectorFeeDetails::from(fee_details));
95        reward
96    }
97
98    pub fn calculate_reward_and_burn_fee_details(
99        &self,
100        fee_details: &CollectorFeeDetails,
101    ) -> FeeDistribution {
102        let burn = fee_details.transaction_fee * self.burn_percent() / 100;
103        let deposit = fee_details
104            .priority_fee
105            .saturating_add(fee_details.transaction_fee.saturating_sub(burn));
106        FeeDistribution { deposit, burn }
107    }
108
109    const fn burn_percent(&self) -> u64 {
110        // NOTE: burn percent is statically 50%, in case it needs to change in the future,
111        // burn_percent can be bank property that being passed down from bank to bank, without
112        // needing fee-rate-governor
113        static_assertions::const_assert!(solana_fee_calculator::DEFAULT_BURN_PERCENT <= 100);
114
115        solana_fee_calculator::DEFAULT_BURN_PERCENT as u64
116    }
117
118    /// Attempts to deposit the given `deposit` amount into the fee collector account.
119    ///
120    /// Returns the original `deposit` amount if the deposit failed and must be burned, otherwise 0.
121    fn deposit_or_burn_fee(&self, deposit: u64) -> u64 {
122        if deposit == 0 {
123            return 0;
124        }
125
126        // Per SIMD-0232: the commission collector address should be fetched
127        // from the state of the vote account at the beginning of the previous
128        // epoch. This is the vote account state used to build the leader
129        // schedule for the current epoch, which *DOES NOT* correspond to
130        // `Bank::current_epoch_stakes()`.
131        let feature_snapshot = self.feature_set.snapshot();
132        let collector_id = if feature_snapshot.custom_commission_collector {
133            let vote_account = self
134                .epoch_stakes
135                .get(&self.epoch)
136                .and_then(|stakes| {
137                    stakes
138                        .stakes()
139                        .vote_accounts()
140                        .get(&self.leader.vote_address)
141                })
142                .expect("The vote account for the leader must exist");
143            // Protection in case the leader is on a vote state without a
144            // collector id, which can happen if a dormant pre-v4 vote state
145            // accrues stake.
146            vote_account
147                .vote_state_view()
148                .block_revenue_collector()
149                .unwrap_or(&self.leader.id)
150        } else {
151            &self.leader.id
152        };
153
154        match self.deposit_fees(collector_id, deposit) {
155            Ok(post_balance) => {
156                self.rewards.write().unwrap().push((
157                    *collector_id,
158                    RewardInfo {
159                        reward_type: RewardType::Fee,
160                        lamports: deposit as i64,
161                        post_balance,
162                        commission_bps: None,
163                    },
164                ));
165                0
166            }
167            Err(err) => {
168                debug!(
169                    "Burned {deposit} lamport tx fee instead of sending to {collector_id} due to \
170                     {err}"
171                );
172                datapoint_warn!(
173                    "bank-burned_fee",
174                    ("slot", self.slot(), i64),
175                    ("num_lamports", deposit, i64),
176                    ("error", err.to_string(), String),
177                );
178                deposit
179            }
180        }
181    }
182
183    // Deposits fees into a specified account and if successful, returns the new balance of that account
184    fn deposit_fees(&self, collector_id: &Pubkey, fees: u64) -> Result<u64, DepositFeeError> {
185        let mut account = self
186            .get_account_with_fixed_root_no_cache(collector_id)
187            .unwrap_or_default();
188
189        let feature_snapshot = self.feature_set.snapshot();
190        if feature_snapshot.custom_commission_collector {
191            let pre_lamports = account.lamports();
192            account
193                .checked_add_lamports(fees)
194                .map_err(|_| DepositFeeError::LamportOverflow)?;
195            if collector_id != &self.leader.vote_address {
196                Bank::collector_type_checked(
197                    collector_id,
198                    pre_lamports,
199                    &account,
200                    &self.reserved_account_keys,
201                    &self.rent_collector().rent,
202                    feature_snapshot.relax_post_exec_min_balance_check,
203                )?;
204            }
205        } else {
206            if !system_program::check_id(account.owner()) {
207                return Err(DepositFeeError::InvalidAccountOwner);
208            }
209
210            let pre_balance = account.lamports();
211            let distribution = account.checked_add_lamports(fees);
212            if distribution.is_err() {
213                return Err(DepositFeeError::LamportOverflow);
214            }
215
216            // rent state transition must be checked in case the account receiving the distribution
217            // doesn't exist yet.
218            if check_static_account_rent_state_transition(
219                pre_balance,
220                account.lamports(),
221                account.data().len(),
222                &self.rent_collector().rent,
223                0, // account index isn't relevant and only used for error message
224                feature_snapshot.relax_post_exec_min_balance_check,
225            )
226            .is_err()
227            {
228                return Err(DepositFeeError::InvalidRentPayingAccount);
229            }
230        }
231
232        self.store_account(collector_id, &account);
233        Ok(account.lamports())
234    }
235
236    /// Checks if a collector account adheres to the rules outlined in SIMD-0232:
237    /// * system program owned account
238    /// * rent-exempt after depositing inflation rewards commission
239    /// * not a reserved account
240    ///
241    /// Returns the kind of collector
242    pub(super) fn collector_type_checked(
243        collector_id: &Pubkey,
244        pre_lamports: u64,
245        account: &AccountSharedData,
246        reserved_account_keys: &ReservedAccountKeys,
247        rent: &Rent,
248        relax_post_execution_balance_checks: bool,
249    ) -> Result<ExternalCollectorType, DepositFeeError> {
250        if !system_program::check_id(account.owner()) {
251            return Err(DepositFeeError::InvalidAccountOwner);
252        }
253
254        if reserved_account_keys.is_reserved(collector_id) {
255            return Err(DepositFeeError::ReservedCollector);
256        }
257
258        // Don't perform rent check on the incinerator, so that the deposit
259        // always works. The incinerator is run at the end of a block
260        if *collector_id == incinerator::id() {
261            Ok(ExternalCollectorType::Incinerator)
262        } else {
263            if !rent.is_exempt(account.lamports(), account.data().len())
264                && (!relax_post_execution_balance_checks || pre_lamports == 0)
265            {
266                Err(DepositFeeError::InvalidRentPayingAccount)
267            } else {
268                Ok(ExternalCollectorType::SystemAccount)
269            }
270        }
271    }
272}
273
274#[cfg(test)]
275pub mod tests {
276    use {
277        super::*,
278        crate::genesis_utils::{create_genesis_config, create_genesis_config_with_leader},
279        agave_feature_set::FeatureSet,
280        solana_account::state_traits::StateMut,
281        solana_pubkey as pubkey,
282        solana_rent::Rent,
283        solana_signer::Signer,
284        solana_vote_interface::state::{VoteStateV4, VoteStateVersions},
285        std::sync::{Arc, RwLock},
286        test_case::test_case,
287    };
288
289    #[test]
290    fn test_deposit_or_burn_zero_fee() {
291        let genesis = create_genesis_config(0);
292        let bank = Bank::new_for_tests(&genesis.genesis_config);
293        assert_eq!(bank.deposit_or_burn_fee(0), 0);
294    }
295
296    #[test_case(true; "custom_commission_collector")]
297    #[test_case(false; "no_custom_commission_collector")]
298    fn test_deposit_or_burn_fee(custom_commission_collector: bool) {
299        #[derive(PartialEq)]
300        enum Scenario {
301            Normal,
302            InvalidOwner,
303            RentPayingAccount,
304            NonDefault,
305            VoteAccount,
306            Incinerator,
307        }
308
309        struct TestCase {
310            scenario: Scenario,
311        }
312
313        impl TestCase {
314            fn new(scenario: Scenario) -> Self {
315                Self { scenario }
316            }
317        }
318
319        for test_case in [
320            TestCase::new(Scenario::Normal),
321            TestCase::new(Scenario::InvalidOwner),
322            TestCase::new(Scenario::RentPayingAccount),
323            TestCase::new(Scenario::NonDefault),
324            TestCase::new(Scenario::VoteAccount),
325            TestCase::new(Scenario::Incinerator),
326        ] {
327            if !custom_commission_collector {
328                // Some scenarios don't make sense without a custom collector
329                match test_case.scenario {
330                    Scenario::NonDefault | Scenario::VoteAccount | Scenario::Incinerator => {
331                        continue;
332                    }
333                    Scenario::Normal | Scenario::InvalidOwner | Scenario::RentPayingAccount => {}
334                }
335            }
336            let initial_balance = 1000;
337            let mut genesis =
338                create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
339            let rent = Rent::default();
340            let min_rent_exempt_balance = rent.minimum_balance(0);
341            genesis.genesis_config.rent = rent; // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
342
343            // update collector id at genesis for some cases
344            let maybe_collector_id = if custom_commission_collector {
345                let mut maybe_collector_id = None;
346                for (address, account) in genesis.genesis_config.accounts.iter_mut() {
347                    if account.owner == solana_sdk_ids::vote::id() {
348                        let mut vote_state =
349                            VoteStateV4::deserialize(account.data(), &Pubkey::default()).unwrap();
350                        let collector_id = match test_case.scenario {
351                            Scenario::Normal => vote_state.block_revenue_collector,
352                            Scenario::InvalidOwner
353                            | Scenario::RentPayingAccount
354                            | Scenario::NonDefault => Pubkey::new_unique(),
355                            Scenario::Incinerator => incinerator::id(),
356                            Scenario::VoteAccount => *address,
357                        };
358                        vote_state.block_revenue_collector = collector_id;
359                        maybe_collector_id = Some(collector_id);
360                        let versioned = VoteStateVersions::V4(Box::new(vote_state));
361                        account.set_state(&versioned).unwrap();
362                    }
363                }
364                maybe_collector_id
365            } else {
366                None
367            };
368
369            let mut bank = Bank::new_for_tests(&genesis.genesis_config);
370            let mut feature_set = FeatureSet::all_enabled();
371            if !custom_commission_collector {
372                feature_set.deactivate(&agave_feature_set::custom_commission_collector::id());
373            }
374            bank.feature_set = Arc::new(feature_set);
375
376            let collector_id = maybe_collector_id.unwrap_or(*bank.leader_id());
377
378            let deposit = 100;
379            let mut burn = 100;
380
381            match test_case.scenario {
382                Scenario::RentPayingAccount => {
383                    // ensure that the account is rent-paying
384                    let account = AccountSharedData::new(1, 1_000, &Pubkey::new_unique());
385                    bank.store_account(&collector_id, &account);
386                }
387                Scenario::InvalidOwner => {
388                    // ensure that account owner is invalid and fee distribution will fail
389                    let account =
390                        AccountSharedData::new(min_rent_exempt_balance, 0, &Pubkey::new_unique());
391                    bank.store_account(&collector_id, &account);
392                }
393                Scenario::VoteAccount => {
394                    // nothing to do, collector id already set, and vote account
395                    // already exists
396                }
397                Scenario::Incinerator => {
398                    // nothing to do, incinerator already exists
399                }
400                Scenario::NonDefault | Scenario::Normal => {
401                    let account =
402                        AccountSharedData::new(min_rent_exempt_balance, 0, &system_program::id());
403                    bank.store_account(&collector_id, &account);
404                }
405            }
406
407            let initial_burn = burn;
408            let initial_collector_balance = bank.get_balance(&collector_id);
409            burn += bank.deposit_or_burn_fee(deposit);
410            let new_collector_balance = bank.get_balance(&collector_id);
411
412            match test_case.scenario {
413                Scenario::InvalidOwner | Scenario::RentPayingAccount => {
414                    assert_eq!(initial_collector_balance, new_collector_balance);
415                    assert_eq!(initial_burn + deposit, burn);
416                    let locked_rewards = bank.rewards.read().unwrap();
417                    assert!(
418                        locked_rewards.is_empty(),
419                        "There should be no rewards distributed"
420                    );
421                }
422                Scenario::NonDefault
423                | Scenario::Normal
424                | Scenario::VoteAccount
425                | Scenario::Incinerator => {
426                    assert_eq!(initial_collector_balance + deposit, new_collector_balance);
427
428                    assert_eq!(initial_burn, burn);
429
430                    let locked_rewards = bank.rewards.read().unwrap();
431                    assert_eq!(
432                        locked_rewards.len(),
433                        1,
434                        "There should be one reward distributed"
435                    );
436
437                    let reward_info = &locked_rewards[0];
438                    assert_eq!(
439                        reward_info.1.lamports, deposit as i64,
440                        "The reward amount should match the expected deposit"
441                    );
442                    assert_eq!(
443                        reward_info.1.reward_type,
444                        RewardType::Fee,
445                        "The reward type should be Fee"
446                    );
447                }
448            }
449        }
450    }
451
452    #[test]
453    fn test_deposit_fees() {
454        let initial_balance = 1_000_000_000;
455        let genesis = create_genesis_config(initial_balance);
456        let bank = Bank::new_for_tests(&genesis.genesis_config);
457        let pubkey = genesis.mint_keypair.pubkey();
458        let deposit_amount = 500;
459
460        assert_eq!(
461            bank.deposit_fees(&pubkey, deposit_amount),
462            Ok(initial_balance + deposit_amount),
463            "New balance should be the sum of the initial balance and deposit amount"
464        );
465    }
466
467    #[test]
468    fn test_deposit_fees_with_overflow() {
469        let initial_balance = u64::MAX;
470        let genesis = create_genesis_config(initial_balance);
471        let bank = Bank::new_for_tests(&genesis.genesis_config);
472        let pubkey = genesis.mint_keypair.pubkey();
473        let deposit_amount = 500;
474
475        assert_eq!(
476            bank.deposit_fees(&pubkey, deposit_amount),
477            Err(DepositFeeError::LamportOverflow),
478            "Expected an error due to lamport overflow"
479        );
480    }
481
482    #[test_case(true, Ok(()); "allowed")]
483    #[test_case(false, Err(DepositFeeError::InvalidAccountOwner); "prohibited")]
484    fn test_deposit_fees_to_vote_account(
485        custom_commission_collector: bool,
486        expected: Result<(), DepositFeeError>,
487    ) {
488        let initial_balance = 1000;
489        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
490        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
491        let mut feature_set = FeatureSet::all_enabled();
492        if !custom_commission_collector {
493            feature_set.deactivate(&agave_feature_set::custom_commission_collector::id());
494        }
495        bank.feature_set = Arc::new(feature_set);
496
497        let pubkey = genesis.voting_keypair.pubkey();
498        let deposit_amount = 500;
499        let pre_lamports = bank.get_balance(&pubkey);
500        assert_eq!(
501            expected.map(|_| pre_lamports.saturating_add(deposit_amount)),
502            bank.deposit_fees(&pubkey, deposit_amount)
503        );
504    }
505
506    #[test]
507    fn test_deposit_fees_reserved_account() {
508        let initial_balance = 1000;
509        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
510        let bank = Bank::new_for_tests(&genesis.genesis_config);
511        let deposit_amount = 500;
512
513        for id in bank.get_reserved_account_keys() {
514            assert!(matches!(
515                bank.deposit_fees(id, deposit_amount),
516                Err(DepositFeeError::ReservedCollector) | Err(DepositFeeError::InvalidAccountOwner),
517            ));
518        }
519    }
520
521    #[test]
522    fn test_deposit_fees_to_nonexistent_account_rent_exempt() {
523        let mut genesis = create_genesis_config(0);
524        let rent = Rent::default();
525        genesis.genesis_config.rent = rent.clone();
526        let bank = Bank::new_for_tests(&genesis.genesis_config);
527        let nonexistent_pubkey = Pubkey::new_unique();
528
529        // Fee is sufficient to make the new account rent-exempt
530        let deposit_amount = rent.minimum_balance(0);
531
532        assert!(
533            bank.get_account(&nonexistent_pubkey).is_none(),
534            "Account should not exist before deposit"
535        );
536
537        assert_eq!(
538            bank.deposit_fees(&nonexistent_pubkey, deposit_amount),
539            Ok(deposit_amount),
540            "Deposit should succeed when fee is sufficient for rent-exemption"
541        );
542
543        let account = bank.get_account(&nonexistent_pubkey).unwrap();
544        assert_eq!(account.lamports(), deposit_amount);
545        assert_eq!(account.owner(), &system_program::id());
546    }
547
548    #[test]
549    fn test_deposit_fees_to_nonexistent_account_not_rent_exempt() {
550        let mut genesis = create_genesis_config(0);
551        let rent = Rent::default();
552        genesis.genesis_config.rent = rent.clone();
553        let bank = Bank::new_for_tests(&genesis.genesis_config);
554        let nonexistent_pubkey = Pubkey::new_unique();
555
556        // Fee is insufficient to make the new account rent-exempt
557        let deposit_amount = rent.minimum_balance(0) - 1;
558
559        assert!(
560            bank.get_account(&nonexistent_pubkey).is_none(),
561            "Account should not exist before deposit"
562        );
563
564        assert_eq!(
565            bank.deposit_fees(&nonexistent_pubkey, deposit_amount),
566            Err(DepositFeeError::InvalidRentPayingAccount),
567            "Deposit should fail when fee is insufficient for rent-exemption"
568        );
569
570        assert!(
571            bank.get_account(&nonexistent_pubkey).is_none(),
572            "Account should still not exist after failed deposit"
573        );
574    }
575
576    #[test_case(true; "custom_commission_collector")]
577    #[test_case(false; "no_custom_commission_collector")]
578    fn test_deposit_or_burn_fee_respects_relaxed_post_exec_min_balance_check(
579        custom_commission_collector: bool,
580    ) {
581        enum CollectorState {
582            InitializedToSubRentExemptMinimum,
583            UninitializedToSubRentExemptMinimum,
584            UninitializedToRentExempt,
585        }
586
587        for collector_state in [
588            CollectorState::InitializedToSubRentExemptMinimum,
589            CollectorState::UninitializedToSubRentExemptMinimum,
590            CollectorState::UninitializedToRentExempt,
591        ] {
592            for relax_post_exec_min_balance_check in [false, true] {
593                let mut genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), 1000);
594                let rent = Rent::default();
595                genesis.genesis_config.rent = rent.clone();
596
597                let initialized_data_len = 64;
598                let rent_exempt_minimum = rent.minimum_balance(initialized_data_len);
599                assert!(rent_exempt_minimum > 1);
600
601                let (pre_balance, deposit, should_succeed) = match collector_state {
602                    CollectorState::InitializedToSubRentExemptMinimum => (
603                        rent_exempt_minimum - 2,
604                        1,
605                        relax_post_exec_min_balance_check,
606                    ),
607                    CollectorState::UninitializedToSubRentExemptMinimum => (0, 1, false),
608                    CollectorState::UninitializedToRentExempt => (0, rent_exempt_minimum, true),
609                };
610                let post_balance = pre_balance + deposit;
611                let maybe_collector_id = if custom_commission_collector {
612                    let mut maybe_collector_id = None;
613                    for account in genesis.genesis_config.accounts.values_mut() {
614                        if account.owner == solana_sdk_ids::vote::id() {
615                            let mut vote_state =
616                                VoteStateV4::deserialize(account.data(), &Pubkey::default())
617                                    .unwrap();
618                            let collector_id = Pubkey::new_unique();
619                            vote_state.block_revenue_collector = collector_id;
620                            maybe_collector_id = Some(collector_id);
621                            let versioned = VoteStateVersions::V4(Box::new(vote_state));
622                            account.set_state(&versioned).unwrap();
623                        }
624                    }
625                    maybe_collector_id
626                } else {
627                    None
628                };
629
630                let mut bank = Bank::new_for_tests(&genesis.genesis_config);
631
632                let mut feature_set = FeatureSet::all_enabled();
633                if !custom_commission_collector {
634                    feature_set.deactivate(&agave_feature_set::custom_commission_collector::id());
635                }
636                if !relax_post_exec_min_balance_check {
637                    feature_set
638                        .deactivate(&agave_feature_set::relax_post_exec_min_balance_check::id());
639                }
640                bank.feature_set = Arc::new(feature_set);
641
642                let collector_id = maybe_collector_id.unwrap_or(*bank.leader_id());
643                let account = AccountSharedData::new(
644                    pre_balance,
645                    initialized_data_len,
646                    &system_program::id(),
647                );
648                bank.store_account(&collector_id, &account);
649
650                let burned = bank.deposit_or_burn_fee(deposit);
651                let rewards = bank.rewards.read().unwrap();
652
653                // post simd-392, deposits to existing accounts are always valid because
654                // they are rent-exempt before and after the deposit takes place.
655                // Deposits to uninitialized accounts still must make the account rent-exempt.
656                // pre simd-392, if a deposit to a rent-paying account isn't sufficient to
657                // make it rent-exempt then it fails and the deposit is burned.
658                if should_succeed {
659                    assert_eq!(burned, 0);
660                    assert_eq!(bank.get_balance(&collector_id), post_balance);
661                    assert_eq!(rewards.len(), 1, "fee should be distributed to the leader");
662                    assert_eq!(rewards[0].1.post_balance, post_balance);
663                } else {
664                    assert_eq!(burned, deposit);
665                    assert_eq!(bank.get_balance(&collector_id), pre_balance);
666                    assert!(
667                        rewards.is_empty(),
668                        "fee should be burned when the rent transition is invalid"
669                    );
670                }
671            }
672        }
673    }
674
675    #[test]
676    fn test_distribute_transaction_fee_details_normal() {
677        let initial_balance = 1000;
678        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
679        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
680        let transaction_fee = 100;
681        let priority_fee = 200;
682        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
683            transaction_fee,
684            priority_fee,
685        });
686        let expected_burn = transaction_fee * bank.burn_percent() / 100;
687        let expected_rewards = transaction_fee - expected_burn + priority_fee;
688
689        let collector_id = *bank.leader_id();
690
691        let initial_capitalization = bank.capitalization();
692        let initial_collector_balance = bank.get_balance(&collector_id);
693        bank.distribute_transaction_fee_details();
694        let new_collector_balance = bank.get_balance(&collector_id);
695
696        assert_eq!(
697            initial_collector_balance + expected_rewards,
698            new_collector_balance
699        );
700        assert_eq!(
701            initial_capitalization - expected_burn,
702            bank.capitalization()
703        );
704        let locked_rewards = bank.rewards.read().unwrap();
705        assert_eq!(
706            locked_rewards.len(),
707            1,
708            "There should be one reward distributed"
709        );
710
711        let reward_info = &locked_rewards[0];
712        assert_eq!(
713            reward_info.1.lamports, expected_rewards as i64,
714            "The reward amount should match the expected deposit"
715        );
716        assert_eq!(
717            reward_info.1.reward_type,
718            RewardType::Fee,
719            "The reward type should be Fee"
720        );
721    }
722
723    #[test]
724    fn test_distribute_transaction_fee_details_zero() {
725        let genesis = create_genesis_config(0);
726        let bank = Bank::new_for_tests(&genesis.genesis_config);
727        assert_eq!(
728            *bank.collector_fee_details.read().unwrap(),
729            CollectorFeeDetails::default()
730        );
731
732        let initial_capitalization = bank.capitalization();
733        let initial_leader_id_balance = bank.get_balance(bank.leader_id());
734        bank.distribute_transaction_fee_details();
735        let new_leader_id_balance = bank.get_balance(bank.leader_id());
736
737        assert_eq!(initial_leader_id_balance, new_leader_id_balance);
738        assert_eq!(initial_capitalization, bank.capitalization());
739        let locked_rewards = bank.rewards.read().unwrap();
740        assert!(
741            locked_rewards.is_empty(),
742            "There should be no rewards distributed"
743        );
744    }
745
746    #[test]
747    fn test_distribute_transaction_fee_details_overflow_failure() {
748        let initial_balance = 1000;
749        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
750        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
751        let transaction_fee = 100;
752        let priority_fee = 200;
753        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
754            transaction_fee,
755            priority_fee,
756        });
757
758        let collector_id = *bank.leader_id();
759
760        // ensure that account balance will overflow and fee distribution will fail
761        let mut account = bank.get_account(&collector_id).unwrap_or_default();
762        account.set_lamports(u64::MAX);
763        bank.store_account(&collector_id, &account);
764
765        let initial_capitalization = bank.capitalization();
766        let initial_collector_balance = bank.get_balance(&collector_id);
767        bank.distribute_transaction_fee_details();
768        let new_collector_balance = bank.get_balance(&collector_id);
769
770        assert_eq!(initial_collector_balance, new_collector_balance);
771        assert_eq!(
772            initial_capitalization - transaction_fee - priority_fee,
773            bank.capitalization()
774        );
775        let locked_rewards = bank.rewards.read().unwrap();
776        assert!(
777            locked_rewards.is_empty(),
778            "There should be no rewards distributed"
779        );
780    }
781}