solana_runtime/bank/
fee_distribution.rs

1use {
2    super::Bank,
3    crate::bank::CollectorFeeDetails,
4    log::{debug, warn},
5    solana_account::{ReadableAccount, WritableAccount},
6    solana_fee::FeeFeatures,
7    solana_fee_structure::FeeBudgetLimits,
8    solana_pubkey::Pubkey,
9    solana_reward_info::{RewardInfo, RewardType},
10    solana_runtime_transaction::transaction_with_meta::TransactionWithMeta,
11    solana_svm_rent_collector::svm_rent_collector::SVMRentCollector,
12    solana_system_interface::program as system_program,
13    solana_vote::vote_account::VoteAccountsHashMap,
14    std::{result::Result, sync::atomic::Ordering::Relaxed},
15    thiserror::Error,
16};
17
18#[derive(Error, Debug, PartialEq)]
19enum DepositFeeError {
20    #[error("fee account became rent paying")]
21    InvalidRentPayingAccount,
22    #[error("lamport overflow")]
23    LamportOverflow,
24    #[error("invalid fee account owner")]
25    InvalidAccountOwner,
26}
27
28#[derive(Default)]
29pub struct FeeDistribution {
30    deposit: u64,
31    burn: u64,
32}
33
34impl FeeDistribution {
35    pub fn get_deposit(&self) -> u64 {
36        self.deposit
37    }
38}
39
40impl Bank {
41    // Distribute collected transaction fees for this slot to collector_id (= current leader).
42    //
43    // Each validator is incentivized to process more transactions to earn more transaction fees.
44    // Transaction fees are rewarded for the computing resource utilization cost, directly
45    // proportional to their actual processing power.
46    //
47    // collector_id is rotated according to stake-weighted leader schedule. So the opportunity of
48    // earning transaction fees are fairly distributed by stake. And missing the opportunity
49    // (not producing a block as a leader) earns nothing. So, being online is incentivized as a
50    // form of transaction fees as well.
51    //
52    // On the other hand, rent fees are distributed under slightly different philosophy, while
53    // still being stake-weighted.
54    // Ref: distribute_rent_to_validators
55    pub(super) fn distribute_transaction_fee_details(&self) {
56        let fee_details = self.collector_fee_details.read().unwrap();
57        if fee_details.total() == 0 {
58            // nothing to distribute, exit early
59            return;
60        }
61
62        let FeeDistribution { deposit, burn } =
63            self.calculate_reward_and_burn_fee_details(&fee_details);
64
65        let total_burn = self.deposit_or_burn_fee(deposit).saturating_add(burn);
66        self.capitalization.fetch_sub(total_burn, Relaxed);
67    }
68
69    pub fn calculate_reward_for_transaction(
70        &self,
71        transaction: &impl TransactionWithMeta,
72        fee_budget_limits: &FeeBudgetLimits,
73    ) -> u64 {
74        let (_last_hash, last_lamports_per_signature) =
75            self.last_blockhash_and_lamports_per_signature();
76        let fee_details = solana_fee::calculate_fee_details(
77            transaction,
78            last_lamports_per_signature == 0,
79            self.fee_structure().lamports_per_signature,
80            fee_budget_limits.prioritization_fee,
81            FeeFeatures::from(self.feature_set.as_ref()),
82        );
83        let FeeDistribution {
84            deposit: reward,
85            burn: _,
86        } = self.calculate_reward_and_burn_fee_details(&CollectorFeeDetails::from(fee_details));
87        reward
88    }
89
90    pub fn calculate_reward_and_burn_fee_details(
91        &self,
92        fee_details: &CollectorFeeDetails,
93    ) -> FeeDistribution {
94        if fee_details.transaction_fee == 0 {
95            return FeeDistribution::default();
96        }
97
98        let burn = fee_details.transaction_fee * self.burn_percent() / 100;
99        let deposit = fee_details
100            .priority_fee
101            .saturating_add(fee_details.transaction_fee.saturating_sub(burn));
102        FeeDistribution { deposit, burn }
103    }
104
105    const fn burn_percent(&self) -> u64 {
106        // NOTE: burn percent is statically 50%, in case it needs to change in the future,
107        // burn_percent can be bank property that being passed down from bank to bank, without
108        // needing fee-rate-governor
109        static_assertions::const_assert!(solana_fee_calculator::DEFAULT_BURN_PERCENT <= 100);
110
111        solana_fee_calculator::DEFAULT_BURN_PERCENT as u64
112    }
113
114    /// Attempts to deposit the given `deposit` amount into the fee collector account.
115    ///
116    /// Returns the original `deposit` amount if the deposit failed and must be burned, otherwise 0.
117    fn deposit_or_burn_fee(&self, deposit: u64) -> u64 {
118        if deposit == 0 {
119            return 0;
120        }
121
122        match self.deposit_fees(&self.collector_id, deposit) {
123            Ok(post_balance) => {
124                self.rewards.write().unwrap().push((
125                    self.collector_id,
126                    RewardInfo {
127                        reward_type: RewardType::Fee,
128                        lamports: deposit as i64,
129                        post_balance,
130                        commission: None,
131                    },
132                ));
133                0
134            }
135            Err(err) => {
136                debug!(
137                    "Burned {} lamport tx fee instead of sending to {} due to {}",
138                    deposit, self.collector_id, err
139                );
140                datapoint_warn!(
141                    "bank-burned_fee",
142                    ("slot", self.slot(), i64),
143                    ("num_lamports", deposit, i64),
144                    ("error", err.to_string(), String),
145                );
146                deposit
147            }
148        }
149    }
150
151    // Deposits fees into a specified account and if successful, returns the new balance of that account
152    fn deposit_fees(&self, pubkey: &Pubkey, fees: u64) -> Result<u64, DepositFeeError> {
153        let mut account = self
154            .get_account_with_fixed_root_no_cache(pubkey)
155            .unwrap_or_default();
156
157        if !system_program::check_id(account.owner()) {
158            return Err(DepositFeeError::InvalidAccountOwner);
159        }
160
161        let recipient_pre_rent_state = self.rent_collector().get_account_rent_state(&account);
162        let distribution = account.checked_add_lamports(fees);
163        if distribution.is_err() {
164            return Err(DepositFeeError::LamportOverflow);
165        }
166
167        let recipient_post_rent_state = self.rent_collector().get_account_rent_state(&account);
168        let rent_state_transition_allowed = self
169            .rent_collector()
170            .transition_allowed(&recipient_pre_rent_state, &recipient_post_rent_state);
171        if !rent_state_transition_allowed {
172            return Err(DepositFeeError::InvalidRentPayingAccount);
173        }
174
175        self.store_account(pubkey, &account);
176        Ok(account.lamports())
177    }
178
179    // Distribute collected rent fees for this slot to staked validators (excluding stakers)
180    // according to stake.
181    //
182    // The nature of rent fee is the cost of doing business, every validator has to hold (or have
183    // access to) the same list of accounts, so we pay according to stake, which is a rough proxy for
184    // value to the network.
185    //
186    // Currently, rent distribution doesn't consider given validator's uptime at all (this might
187    // change). That's because rent should be rewarded for the storage resource utilization cost.
188    // It's treated differently from transaction fees, which is for the computing resource
189    // utilization cost.
190    //
191    // We can't use collector_id (which is rotated according to stake-weighted leader schedule)
192    // as an approximation to the ideal rent distribution to simplify and avoid this per-slot
193    // computation for the distribution (time: N log N, space: N acct. stores; N = # of
194    // validators).
195    // The reason is that rent fee doesn't need to be incentivized for throughput unlike transaction
196    // fees
197    //
198    // Ref: distribute_transaction_fee_details
199    fn distribute_rent_to_validators(
200        &self,
201        vote_accounts: &VoteAccountsHashMap,
202        rent_to_be_distributed: u64,
203    ) {
204        let mut total_staked = 0;
205
206        // Collect the stake associated with each validator.
207        // Note that a validator may be present in this vector multiple times if it happens to have
208        // more than one staked vote account somehow
209        let mut validator_stakes = vote_accounts
210            .iter()
211            .filter_map(|(_vote_pubkey, (staked, account))| {
212                if *staked == 0 {
213                    None
214                } else {
215                    total_staked += *staked;
216                    Some((*account.node_pubkey(), *staked))
217                }
218            })
219            .collect::<Vec<(Pubkey, u64)>>();
220
221        #[cfg(test)]
222        if validator_stakes.is_empty() {
223            // some tests bank.freezes() with bad staking state
224            self.capitalization
225                .fetch_sub(rent_to_be_distributed, Relaxed);
226            return;
227        }
228        #[cfg(not(test))]
229        assert!(!validator_stakes.is_empty());
230
231        // Sort first by stake and then by validator identity pubkey for determinism.
232        // If two items are still equal, their relative order does not matter since
233        // both refer to the same validator.
234        validator_stakes.sort_unstable_by(|(pubkey1, staked1), (pubkey2, staked2)| {
235            (staked1, pubkey1).cmp(&(staked2, pubkey2)).reverse()
236        });
237
238        let mut rent_distributed_in_initial_round = 0;
239        let validator_rent_shares = validator_stakes
240            .into_iter()
241            .map(|(pubkey, staked)| {
242                let rent_share = (((staked as u128) * (rent_to_be_distributed as u128))
243                    / (total_staked as u128))
244                    .try_into()
245                    .unwrap();
246                rent_distributed_in_initial_round += rent_share;
247                (pubkey, rent_share)
248            })
249            .collect::<Vec<(Pubkey, u64)>>();
250
251        // Leftover lamports after fraction calculation, will be paid to validators starting from highest stake
252        // holder
253        let mut leftover_lamports = rent_to_be_distributed - rent_distributed_in_initial_round;
254
255        let mut rent_to_burn: u64 = 0;
256        let mut rewards = vec![];
257        validator_rent_shares
258            .into_iter()
259            .for_each(|(pubkey, rent_share)| {
260                let rent_to_be_paid = if leftover_lamports > 0 {
261                    leftover_lamports -= 1;
262                    rent_share + 1
263                } else {
264                    rent_share
265                };
266                if rent_to_be_paid > 0 {
267                    match self.deposit_fees(&pubkey, rent_to_be_paid) {
268                        Ok(post_balance) => {
269                            rewards.push((
270                                pubkey,
271                                RewardInfo {
272                                    reward_type: RewardType::Rent,
273                                    lamports: rent_to_be_paid as i64,
274                                    post_balance,
275                                    commission: None,
276                                },
277                            ));
278                        }
279                        Err(err) => {
280                            debug!(
281                                "Burned {} lamport rent fee instead of sending to {} due to {}",
282                                rent_to_be_paid, pubkey, err
283                            );
284
285                            // overflow adding lamports or resulting account is invalid
286                            // so burn lamports and track lamports burned per slot
287                            rent_to_burn = rent_to_burn.saturating_add(rent_to_be_paid);
288                        }
289                    }
290                }
291            });
292        self.rewards.write().unwrap().append(&mut rewards);
293
294        if rent_to_burn > 0 {
295            self.capitalization.fetch_sub(rent_to_burn, Relaxed);
296            datapoint_warn!(
297                "bank-burned_rent",
298                ("slot", self.slot(), i64),
299                ("num_lamports", rent_to_burn, i64)
300            );
301        }
302
303        assert_eq!(leftover_lamports, 0);
304    }
305
306    pub(super) fn distribute_rent_fees(&self) {
307        let total_rent_collected = self.collected_rent.load(Relaxed);
308
309        if !self.should_collect_rent() {
310            if total_rent_collected != 0 {
311                warn!("Rent fees collection is disabled, yet total rent collected was non zero! Total rent collected: {total_rent_collected}");
312            }
313            return;
314        }
315
316        let (burned_portion, rent_to_be_distributed) = self
317            .rent_collector
318            .rent
319            .calculate_burn(total_rent_collected);
320
321        debug!(
322            "distributed rent: {} (rounded from: {}, burned: {})",
323            rent_to_be_distributed, total_rent_collected, burned_portion
324        );
325        self.capitalization.fetch_sub(burned_portion, Relaxed);
326
327        if rent_to_be_distributed == 0 {
328            return;
329        }
330
331        self.distribute_rent_to_validators(&self.vote_accounts(), rent_to_be_distributed);
332    }
333}
334
335#[cfg(test)]
336pub mod tests {
337    use {
338        super::*,
339        crate::genesis_utils::{
340            create_genesis_config, create_genesis_config_with_leader,
341            create_genesis_config_with_vote_accounts, ValidatorVoteKeypairs,
342        },
343        solana_account::AccountSharedData,
344        solana_native_token::sol_to_lamports,
345        solana_pubkey as pubkey,
346        solana_rent::Rent,
347        solana_signer::Signer,
348        solana_svm_rent_collector::rent_state::RentState,
349        std::sync::RwLock,
350    };
351
352    #[test]
353    fn test_deposit_or_burn_zero_fee() {
354        let genesis = create_genesis_config(0);
355        let bank = Bank::new_for_tests(&genesis.genesis_config);
356        assert_eq!(bank.deposit_or_burn_fee(0), 0);
357    }
358
359    #[test]
360    fn test_deposit_or_burn_fee() {
361        #[derive(PartialEq)]
362        enum Scenario {
363            Normal,
364            InvalidOwner,
365            RentPaying,
366        }
367
368        struct TestCase {
369            scenario: Scenario,
370        }
371
372        impl TestCase {
373            fn new(scenario: Scenario) -> Self {
374                Self { scenario }
375            }
376        }
377
378        for test_case in [
379            TestCase::new(Scenario::Normal),
380            TestCase::new(Scenario::InvalidOwner),
381            TestCase::new(Scenario::RentPaying),
382        ] {
383            let mut genesis = create_genesis_config(0);
384            let rent = Rent::default();
385            let min_rent_exempt_balance = rent.minimum_balance(0);
386            genesis.genesis_config.rent = rent; // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
387            let bank = Bank::new_for_tests(&genesis.genesis_config);
388
389            let deposit = 100;
390            let mut burn = 100;
391
392            if test_case.scenario == Scenario::RentPaying {
393                // ensure that account balance + collected fees will make it rent-paying
394                let initial_balance = 100;
395                let account = AccountSharedData::new(initial_balance, 0, &system_program::id());
396                bank.store_account(bank.collector_id(), &account);
397                assert!(initial_balance + deposit < min_rent_exempt_balance);
398            } else if test_case.scenario == Scenario::InvalidOwner {
399                // ensure that account owner is invalid and fee distribution will fail
400                let account =
401                    AccountSharedData::new(min_rent_exempt_balance, 0, &Pubkey::new_unique());
402                bank.store_account(bank.collector_id(), &account);
403            } else {
404                let account =
405                    AccountSharedData::new(min_rent_exempt_balance, 0, &system_program::id());
406                bank.store_account(bank.collector_id(), &account);
407            }
408
409            let initial_burn = burn;
410            let initial_collector_id_balance = bank.get_balance(bank.collector_id());
411            burn += bank.deposit_or_burn_fee(deposit);
412            let new_collector_id_balance = bank.get_balance(bank.collector_id());
413
414            if test_case.scenario != Scenario::Normal {
415                assert_eq!(initial_collector_id_balance, new_collector_id_balance);
416                assert_eq!(initial_burn + deposit, burn);
417                let locked_rewards = bank.rewards.read().unwrap();
418                assert!(
419                    locked_rewards.is_empty(),
420                    "There should be no rewards distributed"
421                );
422            } else {
423                assert_eq!(
424                    initial_collector_id_balance + deposit,
425                    new_collector_id_balance
426                );
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    #[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]
482    fn test_deposit_fees_invalid_account_owner() {
483        let initial_balance = 1000;
484        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
485        let bank = Bank::new_for_tests(&genesis.genesis_config);
486        let pubkey = genesis.voting_keypair.pubkey();
487        let deposit_amount = 500;
488
489        assert_eq!(
490            bank.deposit_fees(&pubkey, deposit_amount),
491            Err(DepositFeeError::InvalidAccountOwner),
492            "Expected an error due to invalid account owner"
493        );
494    }
495
496    #[test]
497    fn test_deposit_fees_invalid_rent_paying() {
498        let initial_balance = 0;
499        let genesis = create_genesis_config(initial_balance);
500        let pubkey = genesis.mint_keypair.pubkey();
501        let mut genesis_config = genesis.genesis_config;
502        genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
503        let bank = Bank::new_for_tests(&genesis_config);
504        let min_rent_exempt_balance = genesis_config.rent.minimum_balance(0);
505
506        let deposit_amount = 500;
507        assert!(initial_balance + deposit_amount < min_rent_exempt_balance);
508
509        assert_eq!(
510            bank.deposit_fees(&pubkey, deposit_amount),
511            Err(DepositFeeError::InvalidRentPayingAccount),
512            "Expected an error due to invalid rent paying account"
513        );
514    }
515
516    #[test]
517    fn test_distribute_rent_to_validators_rent_paying() {
518        solana_logger::setup();
519
520        const RENT_PER_VALIDATOR: u64 = 55;
521        const TOTAL_RENT: u64 = RENT_PER_VALIDATOR * 4;
522
523        let empty_validator = ValidatorVoteKeypairs::new_rand();
524        let rent_paying_validator = ValidatorVoteKeypairs::new_rand();
525        let becomes_rent_exempt_validator = ValidatorVoteKeypairs::new_rand();
526        let rent_exempt_validator = ValidatorVoteKeypairs::new_rand();
527        let keypairs = vec![
528            &empty_validator,
529            &rent_paying_validator,
530            &becomes_rent_exempt_validator,
531            &rent_exempt_validator,
532        ];
533        let genesis_config_info = create_genesis_config_with_vote_accounts(
534            sol_to_lamports(1000.),
535            &keypairs,
536            vec![sol_to_lamports(1000.); 4],
537        );
538        let mut genesis_config = genesis_config_info.genesis_config;
539        genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
540
541        let bank = Bank::new_for_tests(&genesis_config);
542        let rent_exempt_minimum = bank.rent_collector().get_rent().minimum_balance(0);
543
544        // Make one validator have an empty identity account
545        let mut empty_validator_account = bank
546            .get_account_with_fixed_root(&empty_validator.node_keypair.pubkey())
547            .unwrap();
548        empty_validator_account.set_lamports(0);
549        bank.store_account(
550            &empty_validator.node_keypair.pubkey(),
551            &empty_validator_account,
552        );
553
554        // Make one validator almost rent-exempt, less RENT_PER_VALIDATOR
555        let mut becomes_rent_exempt_validator_account = bank
556            .get_account_with_fixed_root(&becomes_rent_exempt_validator.node_keypair.pubkey())
557            .unwrap();
558        becomes_rent_exempt_validator_account
559            .set_lamports(rent_exempt_minimum - RENT_PER_VALIDATOR);
560        bank.store_account(
561            &becomes_rent_exempt_validator.node_keypair.pubkey(),
562            &becomes_rent_exempt_validator_account,
563        );
564
565        // Make one validator rent-exempt
566        let mut rent_exempt_validator_account = bank
567            .get_account_with_fixed_root(&rent_exempt_validator.node_keypair.pubkey())
568            .unwrap();
569        rent_exempt_validator_account.set_lamports(rent_exempt_minimum);
570        bank.store_account(
571            &rent_exempt_validator.node_keypair.pubkey(),
572            &rent_exempt_validator_account,
573        );
574
575        let get_rent_state = |bank: &Bank, address: &Pubkey| -> RentState {
576            let account = bank
577                .get_account_with_fixed_root(address)
578                .unwrap_or_default();
579            bank.rent_collector().get_account_rent_state(&account)
580        };
581
582        // Assert starting RentStates
583        assert_eq!(
584            get_rent_state(&bank, &empty_validator.node_keypair.pubkey()),
585            RentState::Uninitialized
586        );
587        assert_eq!(
588            get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()),
589            RentState::RentPaying {
590                lamports: 42,
591                data_size: 0,
592            }
593        );
594        assert_eq!(
595            get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()),
596            RentState::RentPaying {
597                lamports: rent_exempt_minimum - RENT_PER_VALIDATOR,
598                data_size: 0,
599            }
600        );
601        assert_eq!(
602            get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()),
603            RentState::RentExempt
604        );
605
606        let old_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey());
607        let old_rent_paying_validator_lamports =
608            bank.get_balance(&rent_paying_validator.node_keypair.pubkey());
609        let old_becomes_rent_exempt_validator_lamports =
610            bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey());
611        let old_rent_exempt_validator_lamports =
612            bank.get_balance(&rent_exempt_validator.node_keypair.pubkey());
613
614        bank.distribute_rent_to_validators(&bank.vote_accounts(), TOTAL_RENT);
615
616        let new_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey());
617        let new_rent_paying_validator_lamports =
618            bank.get_balance(&rent_paying_validator.node_keypair.pubkey());
619        let new_becomes_rent_exempt_validator_lamports =
620            bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey());
621        let new_rent_exempt_validator_lamports =
622            bank.get_balance(&rent_exempt_validator.node_keypair.pubkey());
623
624        // Assert ending balances; rent should be withheld if test is active and ending RentState
625        // is RentPaying, ie. empty_validator and rent_paying_validator
626        assert_eq!(old_empty_validator_lamports, new_empty_validator_lamports);
627
628        assert_eq!(
629            old_rent_paying_validator_lamports,
630            new_rent_paying_validator_lamports
631        );
632
633        assert_eq!(
634            old_becomes_rent_exempt_validator_lamports + RENT_PER_VALIDATOR,
635            new_becomes_rent_exempt_validator_lamports
636        );
637
638        assert_eq!(
639            old_rent_exempt_validator_lamports + RENT_PER_VALIDATOR,
640            new_rent_exempt_validator_lamports
641        );
642
643        // Assert ending RentStates
644        assert_eq!(
645            RentState::Uninitialized,
646            get_rent_state(&bank, &empty_validator.node_keypair.pubkey()),
647        );
648        assert_eq!(
649            RentState::RentPaying {
650                lamports: old_rent_paying_validator_lamports,
651                data_size: 0,
652            },
653            get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()),
654        );
655        assert_eq!(
656            RentState::RentExempt,
657            get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()),
658        );
659        assert_eq!(
660            RentState::RentExempt,
661            get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()),
662        );
663    }
664
665    #[test]
666    fn test_distribute_rent_to_validators_invalid_owner() {
667        struct TestCase {
668            use_invalid_owner: bool,
669        }
670
671        impl TestCase {
672            fn new(use_invalid_owner: bool) -> Self {
673                Self { use_invalid_owner }
674            }
675        }
676
677        for test_case in [TestCase::new(false), TestCase::new(true)] {
678            let genesis_config_info =
679                create_genesis_config_with_leader(0, &Pubkey::new_unique(), 100);
680            let mut genesis_config = genesis_config_info.genesis_config;
681            genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
682
683            let bank = Bank::new_for_tests(&genesis_config);
684
685            let initial_balance = 1_000_000;
686            let account_owner = if test_case.use_invalid_owner {
687                Pubkey::new_unique()
688            } else {
689                system_program::id()
690            };
691            let account = AccountSharedData::new(initial_balance, 0, &account_owner);
692            bank.store_account(bank.collector_id(), &account);
693
694            let initial_capitalization = bank.capitalization();
695            let rent_fees = 100;
696            bank.distribute_rent_to_validators(&bank.vote_accounts(), rent_fees);
697            let new_capitalization = bank.capitalization();
698            let new_balance = bank.get_balance(bank.collector_id());
699
700            if test_case.use_invalid_owner {
701                assert_eq!(initial_balance, new_balance);
702                assert_eq!(initial_capitalization - rent_fees, new_capitalization);
703                assert_eq!(bank.rewards.read().unwrap().len(), 0);
704            } else {
705                assert_eq!(initial_balance + rent_fees, new_balance);
706                assert_eq!(initial_capitalization, new_capitalization);
707                assert_eq!(bank.rewards.read().unwrap().len(), 1);
708            }
709        }
710    }
711
712    #[test]
713    fn test_distribute_transaction_fee_details_normal() {
714        let genesis = create_genesis_config(0);
715        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
716        let transaction_fee = 100;
717        let priority_fee = 200;
718        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
719            transaction_fee,
720            priority_fee,
721        });
722        let expected_burn = transaction_fee * bank.burn_percent() / 100;
723        let expected_rewards = transaction_fee - expected_burn + priority_fee;
724
725        let initial_capitalization = bank.capitalization();
726        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
727        bank.distribute_transaction_fee_details();
728        let new_collector_id_balance = bank.get_balance(bank.collector_id());
729
730        assert_eq!(
731            initial_collector_id_balance + expected_rewards,
732            new_collector_id_balance
733        );
734        assert_eq!(
735            initial_capitalization - expected_burn,
736            bank.capitalization()
737        );
738        let locked_rewards = bank.rewards.read().unwrap();
739        assert_eq!(
740            locked_rewards.len(),
741            1,
742            "There should be one reward distributed"
743        );
744
745        let reward_info = &locked_rewards[0];
746        assert_eq!(
747            reward_info.1.lamports, expected_rewards as i64,
748            "The reward amount should match the expected deposit"
749        );
750        assert_eq!(
751            reward_info.1.reward_type,
752            RewardType::Fee,
753            "The reward type should be Fee"
754        );
755    }
756
757    #[test]
758    fn test_distribute_transaction_fee_details_zero() {
759        let genesis = create_genesis_config(0);
760        let bank = Bank::new_for_tests(&genesis.genesis_config);
761        assert_eq!(
762            *bank.collector_fee_details.read().unwrap(),
763            CollectorFeeDetails::default()
764        );
765
766        let initial_capitalization = bank.capitalization();
767        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
768        bank.distribute_transaction_fee_details();
769        let new_collector_id_balance = bank.get_balance(bank.collector_id());
770
771        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
772        assert_eq!(initial_capitalization, bank.capitalization());
773        let locked_rewards = bank.rewards.read().unwrap();
774        assert!(
775            locked_rewards.is_empty(),
776            "There should be no rewards distributed"
777        );
778    }
779
780    #[test]
781    fn test_distribute_transaction_fee_details_overflow_failure() {
782        let genesis = create_genesis_config(0);
783        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
784        let transaction_fee = 100;
785        let priority_fee = 200;
786        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
787            transaction_fee,
788            priority_fee,
789        });
790
791        // ensure that account balance will overflow and fee distribution will fail
792        let account = AccountSharedData::new(u64::MAX, 0, &system_program::id());
793        bank.store_account(bank.collector_id(), &account);
794
795        let initial_capitalization = bank.capitalization();
796        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
797        bank.distribute_transaction_fee_details();
798        let new_collector_id_balance = bank.get_balance(bank.collector_id());
799
800        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
801        assert_eq!(
802            initial_capitalization - transaction_fee - priority_fee,
803            bank.capitalization()
804        );
805        let locked_rewards = bank.rewards.read().unwrap();
806        assert!(
807            locked_rewards.is_empty(),
808            "There should be no rewards distributed"
809        );
810    }
811}