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 =
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; 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 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 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(); 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 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}