atlas_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 =
157            get_account_rent_state(&self.rent_collector().rent, &account);
158        let distribution = account.checked_add_lamports(fees);
159        if distribution.is_err() {
160            return Err(DepositFeeError::LamportOverflow);
161        }
162
163        let recipient_post_rent_state =
164            get_account_rent_state(&self.rent_collector().rent, &account);
165        let rent_state_transition_allowed =
166            transition_allowed(&recipient_pre_rent_state, &recipient_post_rent_state);
167        if !rent_state_transition_allowed {
168            return Err(DepositFeeError::InvalidRentPayingAccount);
169        }
170
171        self.store_account(pubkey, &account);
172        Ok(account.lamports())
173    }
174}
175
176#[cfg(test)]
177pub mod tests {
178    use {
179        super::*,
180        crate::genesis_utils::{create_genesis_config, create_genesis_config_with_leader},
181        solana_account::AccountSharedData,
182        atlas_pubkey as pubkey,
183        solana_rent::Rent,
184        solana_signer::Signer,
185        std::sync::RwLock,
186    };
187
188    #[test]
189    fn test_deposit_or_burn_zero_fee() {
190        let genesis = create_genesis_config(0);
191        let bank = Bank::new_for_tests(&genesis.genesis_config);
192        assert_eq!(bank.deposit_or_burn_fee(0), 0);
193    }
194
195    #[test]
196    fn test_deposit_or_burn_fee() {
197        #[derive(PartialEq)]
198        enum Scenario {
199            Normal,
200            InvalidOwner,
201            RentPaying,
202        }
203
204        struct TestCase {
205            scenario: Scenario,
206        }
207
208        impl TestCase {
209            fn new(scenario: Scenario) -> Self {
210                Self { scenario }
211            }
212        }
213
214        for test_case in [
215            TestCase::new(Scenario::Normal),
216            TestCase::new(Scenario::InvalidOwner),
217            TestCase::new(Scenario::RentPaying),
218        ] {
219            let mut genesis = create_genesis_config(0);
220            let rent = Rent::default();
221            let min_rent_exempt_balance = rent.minimum_balance(0);
222            genesis.genesis_config.rent = rent; // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
223            let bank = Bank::new_for_tests(&genesis.genesis_config);
224
225            let deposit = 100;
226            let mut burn = 100;
227
228            if test_case.scenario == Scenario::RentPaying {
229                // ensure that account balance + collected fees will make it rent-paying
230                let initial_balance = 100;
231                let account = AccountSharedData::new(initial_balance, 0, &system_program::id());
232                bank.store_account(bank.collector_id(), &account);
233                assert!(initial_balance + deposit < min_rent_exempt_balance);
234            } else if test_case.scenario == Scenario::InvalidOwner {
235                // ensure that account owner is invalid and fee distribution will fail
236                let account =
237                    AccountSharedData::new(min_rent_exempt_balance, 0, &Pubkey::new_unique());
238                bank.store_account(bank.collector_id(), &account);
239            } else {
240                let account =
241                    AccountSharedData::new(min_rent_exempt_balance, 0, &system_program::id());
242                bank.store_account(bank.collector_id(), &account);
243            }
244
245            let initial_burn = burn;
246            let initial_collector_id_balance = bank.get_balance(bank.collector_id());
247            burn += bank.deposit_or_burn_fee(deposit);
248            let new_collector_id_balance = bank.get_balance(bank.collector_id());
249
250            if test_case.scenario != Scenario::Normal {
251                assert_eq!(initial_collector_id_balance, new_collector_id_balance);
252                assert_eq!(initial_burn + deposit, burn);
253                let locked_rewards = bank.rewards.read().unwrap();
254                assert!(
255                    locked_rewards.is_empty(),
256                    "There should be no rewards distributed"
257                );
258            } else {
259                assert_eq!(
260                    initial_collector_id_balance + deposit,
261                    new_collector_id_balance
262                );
263
264                assert_eq!(initial_burn, burn);
265
266                let locked_rewards = bank.rewards.read().unwrap();
267                assert_eq!(
268                    locked_rewards.len(),
269                    1,
270                    "There should be one reward distributed"
271                );
272
273                let reward_info = &locked_rewards[0];
274                assert_eq!(
275                    reward_info.1.lamports, deposit as i64,
276                    "The reward amount should match the expected deposit"
277                );
278                assert_eq!(
279                    reward_info.1.reward_type,
280                    RewardType::Fee,
281                    "The reward type should be Fee"
282                );
283            }
284        }
285    }
286
287    #[test]
288    fn test_deposit_fees() {
289        let initial_balance = 1_000_000_000;
290        let genesis = create_genesis_config(initial_balance);
291        let bank = Bank::new_for_tests(&genesis.genesis_config);
292        let pubkey = genesis.mint_keypair.pubkey();
293        let deposit_amount = 500;
294
295        assert_eq!(
296            bank.deposit_fees(&pubkey, deposit_amount),
297            Ok(initial_balance + deposit_amount),
298            "New balance should be the sum of the initial balance and deposit amount"
299        );
300    }
301
302    #[test]
303    fn test_deposit_fees_with_overflow() {
304        let initial_balance = u64::MAX;
305        let genesis = create_genesis_config(initial_balance);
306        let bank = Bank::new_for_tests(&genesis.genesis_config);
307        let pubkey = genesis.mint_keypair.pubkey();
308        let deposit_amount = 500;
309
310        assert_eq!(
311            bank.deposit_fees(&pubkey, deposit_amount),
312            Err(DepositFeeError::LamportOverflow),
313            "Expected an error due to lamport overflow"
314        );
315    }
316
317    #[test]
318    fn test_deposit_fees_invalid_account_owner() {
319        let initial_balance = 1000;
320        let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance);
321        let bank = Bank::new_for_tests(&genesis.genesis_config);
322        let pubkey = genesis.voting_keypair.pubkey();
323        let deposit_amount = 500;
324
325        assert_eq!(
326            bank.deposit_fees(&pubkey, deposit_amount),
327            Err(DepositFeeError::InvalidAccountOwner),
328            "Expected an error due to invalid account owner"
329        );
330    }
331
332    #[test]
333    fn test_deposit_fees_invalid_rent_paying() {
334        let initial_balance = 0;
335        let genesis = create_genesis_config(initial_balance);
336        let pubkey = genesis.mint_keypair.pubkey();
337        let mut genesis_config = genesis.genesis_config;
338        genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default
339        let bank = Bank::new_for_tests(&genesis_config);
340        let min_rent_exempt_balance = genesis_config.rent.minimum_balance(0);
341
342        let deposit_amount = 500;
343        assert!(initial_balance + deposit_amount < min_rent_exempt_balance);
344
345        assert_eq!(
346            bank.deposit_fees(&pubkey, deposit_amount),
347            Err(DepositFeeError::InvalidRentPayingAccount),
348            "Expected an error due to invalid rent paying account"
349        );
350    }
351
352    #[test]
353    fn test_distribute_transaction_fee_details_normal() {
354        let genesis = create_genesis_config(0);
355        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
356        let transaction_fee = 100;
357        let priority_fee = 200;
358        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
359            transaction_fee,
360            priority_fee,
361        });
362        let expected_burn = transaction_fee * bank.burn_percent() / 100;
363        let expected_rewards = transaction_fee - expected_burn + priority_fee;
364
365        let initial_capitalization = bank.capitalization();
366        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
367        bank.distribute_transaction_fee_details();
368        let new_collector_id_balance = bank.get_balance(bank.collector_id());
369
370        assert_eq!(
371            initial_collector_id_balance + expected_rewards,
372            new_collector_id_balance
373        );
374        assert_eq!(
375            initial_capitalization - expected_burn,
376            bank.capitalization()
377        );
378        let locked_rewards = bank.rewards.read().unwrap();
379        assert_eq!(
380            locked_rewards.len(),
381            1,
382            "There should be one reward distributed"
383        );
384
385        let reward_info = &locked_rewards[0];
386        assert_eq!(
387            reward_info.1.lamports, expected_rewards as i64,
388            "The reward amount should match the expected deposit"
389        );
390        assert_eq!(
391            reward_info.1.reward_type,
392            RewardType::Fee,
393            "The reward type should be Fee"
394        );
395    }
396
397    #[test]
398    fn test_distribute_transaction_fee_details_zero() {
399        let genesis = create_genesis_config(0);
400        let bank = Bank::new_for_tests(&genesis.genesis_config);
401        assert_eq!(
402            *bank.collector_fee_details.read().unwrap(),
403            CollectorFeeDetails::default()
404        );
405
406        let initial_capitalization = bank.capitalization();
407        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
408        bank.distribute_transaction_fee_details();
409        let new_collector_id_balance = bank.get_balance(bank.collector_id());
410
411        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
412        assert_eq!(initial_capitalization, bank.capitalization());
413        let locked_rewards = bank.rewards.read().unwrap();
414        assert!(
415            locked_rewards.is_empty(),
416            "There should be no rewards distributed"
417        );
418    }
419
420    #[test]
421    fn test_distribute_transaction_fee_details_overflow_failure() {
422        let genesis = create_genesis_config(0);
423        let mut bank = Bank::new_for_tests(&genesis.genesis_config);
424        let transaction_fee = 100;
425        let priority_fee = 200;
426        bank.collector_fee_details = RwLock::new(CollectorFeeDetails {
427            transaction_fee,
428            priority_fee,
429        });
430
431        // ensure that account balance will overflow and fee distribution will fail
432        let account = AccountSharedData::new(u64::MAX, 0, &system_program::id());
433        bank.store_account(bank.collector_id(), &account);
434
435        let initial_capitalization = bank.capitalization();
436        let initial_collector_id_balance = bank.get_balance(bank.collector_id());
437        bank.distribute_transaction_fee_details();
438        let new_collector_id_balance = bank.get_balance(bank.collector_id());
439
440        assert_eq!(initial_collector_id_balance, new_collector_id_balance);
441        assert_eq!(
442            initial_capitalization - transaction_fee - priority_fee,
443            bank.capitalization()
444        );
445        let locked_rewards = bank.rewards.read().unwrap();
446        assert!(
447            locked_rewards.is_empty(),
448            "There should be no rewards distributed"
449        );
450    }
451}