Skip to main content

solana_runtime/bank/
fee_distribution.rs

1use {
2    super::Bank,
3    crate::bank::CollectorFeeDetails,
4    log::debug,
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_calculator::{get_account_rent_state, transition_allowed},
12    solana_system_interface::program as system_program,
13    std::{result::Result, sync::atomic::Ordering::Relaxed},
14    thiserror::Error,
15};
16
17#[derive(Error, Debug, PartialEq)]
18enum DepositFeeError {
19    #[error("fee account became rent paying")]
20    InvalidRentPayingAccount,
21    #[error("lamport overflow")]
22    LamportOverflow,
23    #[error("invalid fee account owner")]
24    InvalidAccountOwner,
25}
26
27#[derive(Default)]
28pub struct FeeDistribution {
29    deposit: u64,
30    burn: u64,
31}
32
33impl FeeDistribution {
34    pub fn get_deposit(&self) -> u64 {
35        self.deposit
36    }
37}
38
39impl Bank {
40    // Distribute collected transaction fees for this slot to collector_id (= current leader).
41    //
42    // Each validator is incentivized to process more transactions to earn more transaction fees.
43    // Transaction fees are rewarded for the computing resource utilization cost, directly
44    // proportional to their actual processing power.
45    //
46    // collector_id is rotated according to stake-weighted leader schedule. So the opportunity of
47    // earning transaction fees are fairly distributed by stake. And missing the opportunity
48    // (not producing a block as a leader) earns nothing. So, being online is incentivized as a
49    // form of transaction fees as well.
50    pub(super) fn distribute_transaction_fee_details(&self) {
51        let fee_details = self.collector_fee_details.read().unwrap();
52        if fee_details.total_transaction_fee() == 0 {
53            // nothing to distribute, exit early
54            return;
55        }
56
57        let FeeDistribution { deposit, burn } =
58            self.calculate_reward_and_burn_fee_details(&fee_details);
59
60        let total_burn = self.deposit_or_burn_fee(deposit).saturating_add(burn);
61        self.capitalization.fetch_sub(total_burn, Relaxed);
62    }
63
64    pub fn calculate_reward_for_transaction(
65        &self,
66        transaction: &impl TransactionWithMeta,
67        fee_budget_limits: &FeeBudgetLimits,
68    ) -> u64 {
69        let (_last_hash, last_lamports_per_signature) =
70            self.last_blockhash_and_lamports_per_signature();
71        let fee_details = solana_fee::calculate_fee_details(
72            transaction,
73            last_lamports_per_signature == 0,
74            self.fee_structure().lamports_per_signature,
75            fee_budget_limits.prioritization_fee,
76            FeeFeatures::from(self.feature_set.as_ref()),
77        );
78        let FeeDistribution {
79            deposit: reward,
80            burn: _,
81        } = self.calculate_reward_and_burn_fee_details(&CollectorFeeDetails::from(fee_details));
82        reward
83    }
84
85    pub fn calculate_reward_and_burn_fee_details(
86        &self,
87        fee_details: &CollectorFeeDetails,
88    ) -> FeeDistribution {
89        if fee_details.transaction_fee == 0 {
90            return FeeDistribution::default();
91        }
92
93        let burn = fee_details.transaction_fee * self.burn_percent() / 100;
94        let deposit = fee_details
95            .priority_fee
96            .saturating_add(fee_details.transaction_fee.saturating_sub(burn));
97        FeeDistribution { deposit, burn }
98    }
99
100    const fn burn_percent(&self) -> u64 {
101        // NOTE: burn percent is statically 50%, in case it needs to change in the future,
102        // burn_percent can be bank property that being passed down from bank to bank, without
103        // needing fee-rate-governor
104        static_assertions::const_assert!(solana_fee_calculator::DEFAULT_BURN_PERCENT <= 100);
105
106        solana_fee_calculator::DEFAULT_BURN_PERCENT as u64
107    }
108
109    /// Attempts to deposit the given `deposit` amount into the fee collector account.
110    ///
111    /// Returns the original `deposit` amount if the deposit failed and must be burned, otherwise 0.
112    fn deposit_or_burn_fee(&self, deposit: u64) -> u64 {
113        if deposit == 0 {
114            return 0;
115        }
116
117        match self.deposit_fees(&self.collector_id, deposit) {
118            Ok(post_balance) => {
119                self.rewards.write().unwrap().push((
120                    self.collector_id,
121                    RewardInfo {
122                        reward_type: RewardType::Fee,
123                        lamports: deposit as i64,
124                        post_balance,
125                        commission: None,
126                    },
127                ));
128                0
129            }
130            Err(err) => {
131                debug!(
132                    "Burned {} lamport tx fee instead of sending to {} due to {}",
133                    deposit, self.collector_id, err
134                );
135                datapoint_warn!(
136                    "bank-burned_fee",
137                    ("slot", self.slot(), i64),
138                    ("num_lamports", deposit, i64),
139                    ("error", err.to_string(), String),
140                );
141                deposit
142            }
143        }
144    }
145
146    // Deposits fees into a specified account and if successful, returns the new balance of that account
147    fn deposit_fees(&self, pubkey: &Pubkey, fees: u64) -> Result<u64, DepositFeeError> {
148        let mut account = self
149            .get_account_with_fixed_root_no_cache(pubkey)
150            .unwrap_or_default();
151
152        if !system_program::check_id(account.owner()) {
153            return Err(DepositFeeError::InvalidAccountOwner);
154        }
155
156        let recipient_pre_rent_state = get_account_rent_state(
157            &self.rent_collector().rent,
158            account.lamports(),
159            account.data().len(),
160        );
161        let distribution = account.checked_add_lamports(fees);
162        if distribution.is_err() {
163            return Err(DepositFeeError::LamportOverflow);
164        }
165
166        let recipient_post_rent_state = get_account_rent_state(
167            &self.rent_collector().rent,
168            account.lamports(),
169            account.data().len(),
170        );
171        let rent_state_transition_allowed =
172            transition_allowed(&recipient_pre_rent_state, &recipient_post_rent_state);
173        if !rent_state_transition_allowed {
174            return Err(DepositFeeError::InvalidRentPayingAccount);
175        }
176
177        self.store_account(pubkey, &account);
178        Ok(account.lamports())
179    }
180}
181
182#[cfg(test)]
183pub mod tests {
184    use {
185        super::*,
186        crate::genesis_utils::{create_genesis_config, create_genesis_config_with_leader},
187        solana_account::AccountSharedData,
188        solana_pubkey as pubkey,
189        solana_rent::Rent,
190        solana_signer::Signer,
191        std::sync::RwLock,
192    };
193
194    #[test]
195    fn test_deposit_or_burn_zero_fee() {
196        let genesis = create_genesis_config(0);
197        let bank = Bank::new_for_tests(&genesis.genesis_config);
198        assert_eq!(bank.deposit_or_burn_fee(0), 0);
199    }
200
201    #[test]
202    fn test_deposit_or_burn_fee() {
203        #[derive(PartialEq)]
204        enum Scenario {
205            Normal,
206            InvalidOwner,
207            RentPaying,
208        }
209
210        struct TestCase {
211            scenario: Scenario,
212        }
213
214        impl TestCase {
215            fn new(scenario: Scenario) -> Self {
216                Self { scenario }
217            }
218        }
219
220        for test_case in [
221            TestCase::new(Scenario::Normal),
222            TestCase::new(Scenario::InvalidOwner),
223            TestCase::new(Scenario::RentPaying),
224        ] {
225            let mut genesis = create_genesis_config(0);
226            let rent = Rent::default();
227            let min_rent_exempt_balance = rent.minimum_balance(0);
228            genesis.genesis_config.rent = rent; // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
229            let bank = Bank::new_for_tests(&genesis.genesis_config);
230
231            let deposit = 100;
232            let mut burn = 100;
233
234            if test_case.scenario == Scenario::RentPaying {
235                // ensure that account balance + collected fees will make it rent-paying
236                let initial_balance = 100;
237                let account = AccountSharedData::new(initial_balance, 0, &system_program::id());
238                bank.store_account(bank.collector_id(), &account);
239                assert!(initial_balance + deposit < min_rent_exempt_balance);
240            } else if test_case.scenario == Scenario::InvalidOwner {
241                // ensure that account owner is invalid and fee distribution will fail
242                let account =
243                    AccountSharedData::new(min_rent_exempt_balance, 0, &Pubkey::new_unique());
244                bank.store_account(bank.collector_id(), &account);
245            } else {
246                let account =
247                    AccountSharedData::new(min_rent_exempt_balance, 0, &system_program::id());
248                bank.store_account(bank.collector_id(), &account);
249            }
250
251            let initial_burn = burn;
252            let initial_collector_id_balance = bank.get_balance(bank.collector_id());
253            burn += bank.deposit_or_burn_fee(deposit);
254            let new_collector_id_balance = bank.get_balance(bank.collector_id());
255
256            if test_case.scenario != Scenario::Normal {
257                assert_eq!(initial_collector_id_balance, new_collector_id_balance);
258                assert_eq!(initial_burn + deposit, burn);
259                let locked_rewards = bank.rewards.read().unwrap();
260                assert!(
261                    locked_rewards.is_empty(),
262                    "There should be no rewards distributed"
263                );
264            } else {
265                assert_eq!(
266                    initial_collector_id_balance + deposit,
267                    new_collector_id_balance
268                );
269
270                assert_eq!(initial_burn, burn);
271
272                let locked_rewards = bank.rewards.read().unwrap();
273                assert_eq!(
274                    locked_rewards.len(),
275                    1,
276                    "There should be one reward distributed"
277                );
278
279                let reward_info = &locked_rewards[0];
280                assert_eq!(
281                    reward_info.1.lamports, deposit as i64,
282                    "The reward amount should match the expected deposit"
283                );
284                assert_eq!(
285                    reward_info.1.reward_type,
286                    RewardType::Fee,
287                    "The reward type should be Fee"
288                );
289            }
290        }
291    }
292
293    #[test]
294    fn test_deposit_fees() {
295        let initial_balance = 1_000_000_000;
296        let genesis = create_genesis_config(initial_balance);
297        let bank = Bank::new_for_tests(&genesis.genesis_config);
298        let pubkey = genesis.mint_keypair.pubkey();
299        let deposit_amount = 500;
300
301        assert_eq!(
302            bank.deposit_fees(&pubkey, deposit_amount),
303            Ok(initial_balance + deposit_amount),
304            "New balance should be the sum of the initial balance and deposit amount"
305        );
306    }
307
308    #[test]
309    fn test_deposit_fees_with_overflow() {
310        let initial_balance = u64::MAX;
311        let genesis = create_genesis_config(initial_balance);
312        let bank = Bank::new_for_tests(&genesis.genesis_config);
313        let pubkey = genesis.mint_keypair.pubkey();
314        let deposit_amount = 500;
315
316        assert_eq!(
317            bank.deposit_fees(&pubkey, deposit_amount),
318            Err(DepositFeeError::LamportOverflow),
319            "Expected an error due to lamport overflow"
320        );
321    }
322
323    #[test]
324    fn test_deposit_fees_invalid_account_owner() {
325        let initial_balance = 1000;
326        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
327        let bank = Bank::new_for_tests(&genesis.genesis_config);
328        let pubkey = genesis.voting_keypair.pubkey();
329        let deposit_amount = 500;
330
331        assert_eq!(
332            bank.deposit_fees(&pubkey, deposit_amount),
333            Err(DepositFeeError::InvalidAccountOwner),
334            "Expected an error due to invalid account owner"
335        );
336    }
337
338    #[test]
339    fn test_deposit_fees_invalid_rent_paying() {
340        let initial_balance = 0;
341        let genesis = create_genesis_config(initial_balance);
342        let pubkey = genesis.mint_keypair.pubkey();
343        let mut genesis_config = genesis.genesis_config;
344        genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
345        let bank = Bank::new_for_tests(&genesis_config);
346        let min_rent_exempt_balance = genesis_config.rent.minimum_balance(0);
347
348        let deposit_amount = 500;
349        assert!(initial_balance + deposit_amount < min_rent_exempt_balance);
350
351        assert_eq!(
352            bank.deposit_fees(&pubkey, deposit_amount),
353            Err(DepositFeeError::InvalidRentPayingAccount),
354            "Expected an error due to invalid rent paying account"
355        );
356    }
357
358    #[test]
359    fn test_distribute_transaction_fee_details_normal() {
360        let genesis = create_genesis_config(0);
361        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
362        let transaction_fee = 100;
363        let priority_fee = 200;
364        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
365            transaction_fee,
366            priority_fee,
367        });
368        let expected_burn = transaction_fee * bank.burn_percent() / 100;
369        let expected_rewards = transaction_fee - expected_burn + priority_fee;
370
371        let initial_capitalization = bank.capitalization();
372        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
373        bank.distribute_transaction_fee_details();
374        let new_collector_id_balance = bank.get_balance(bank.collector_id());
375
376        assert_eq!(
377            initial_collector_id_balance + expected_rewards,
378            new_collector_id_balance
379        );
380        assert_eq!(
381            initial_capitalization - expected_burn,
382            bank.capitalization()
383        );
384        let locked_rewards = bank.rewards.read().unwrap();
385        assert_eq!(
386            locked_rewards.len(),
387            1,
388            "There should be one reward distributed"
389        );
390
391        let reward_info = &locked_rewards[0];
392        assert_eq!(
393            reward_info.1.lamports, expected_rewards as i64,
394            "The reward amount should match the expected deposit"
395        );
396        assert_eq!(
397            reward_info.1.reward_type,
398            RewardType::Fee,
399            "The reward type should be Fee"
400        );
401    }
402
403    #[test]
404    fn test_distribute_transaction_fee_details_zero() {
405        let genesis = create_genesis_config(0);
406        let bank = Bank::new_for_tests(&genesis.genesis_config);
407        assert_eq!(
408            *bank.collector_fee_details.read().unwrap(),
409            CollectorFeeDetails::default()
410        );
411
412        let initial_capitalization = bank.capitalization();
413        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
414        bank.distribute_transaction_fee_details();
415        let new_collector_id_balance = bank.get_balance(bank.collector_id());
416
417        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
418        assert_eq!(initial_capitalization, bank.capitalization());
419        let locked_rewards = bank.rewards.read().unwrap();
420        assert!(
421            locked_rewards.is_empty(),
422            "There should be no rewards distributed"
423        );
424    }
425
426    #[test]
427    fn test_distribute_transaction_fee_details_overflow_failure() {
428        let genesis = create_genesis_config(0);
429        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
430        let transaction_fee = 100;
431        let priority_fee = 200;
432        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
433            transaction_fee,
434            priority_fee,
435        });
436
437        // ensure that account balance will overflow and fee distribution will fail
438        let account = AccountSharedData::new(u64::MAX, 0, &system_program::id());
439        bank.store_account(bank.collector_id(), &account);
440
441        let initial_capitalization = bank.capitalization();
442        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
443        bank.distribute_transaction_fee_details();
444        let new_collector_id_balance = bank.get_balance(bank.collector_id());
445
446        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
447        assert_eq!(
448            initial_capitalization - transaction_fee - priority_fee,
449            bank.capitalization()
450        );
451        let locked_rewards = bank.rewards.read().unwrap();
452        assert!(
453            locked_rewards.is_empty(),
454            "There should be no rewards distributed"
455        );
456    }
457}