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 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 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 static_assertions::const_assert!(solana_fee_calculator::DEFAULT_BURN_PERCENT <= 100);
105
106 solana_fee_calculator::DEFAULT_BURN_PERCENT as u64
107 }
108
109 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 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; 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 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 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(); 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 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}