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