use { super::Bank, crate::accounts::account_rent_state::RentState, log::{debug, warn}, solana_accounts_db::stake_rewards::RewardInfo, solana_sdk::{ account::{ReadableAccount, WritableAccount}, pubkey::Pubkey, reward_type::RewardType, system_program, }, solana_vote::vote_account::VoteAccountsHashMap, std::{result::Result, sync::atomic::Ordering::Relaxed}, thiserror::Error, }; #[derive(Debug)] struct DepositFeeOptions { check_account_owner: bool, check_rent_paying: bool, } #[derive(Error, Debug, PartialEq)] enum DepositFeeError { #[error("fee account became rent paying")] InvalidRentPayingAccount, #[error("lamport overflow")] LamportOverflow, #[error("invalid fee account owner")] InvalidAccountOwner, } impl Bank { // Distribute collected transaction fees for this slot to collector_id (= current leader). // // Each validator is incentivized to process more transactions to earn more transaction fees. // Transaction fees are rewarded for the computing resource utilization cost, directly // proportional to their actual processing power. // // collector_id is rotated according to stake-weighted leader schedule. So the opportunity of // earning transaction fees are fairly distributed by stake. And missing the opportunity // (not producing a block as a leader) earns nothing. So, being online is incentivized as a // form of transaction fees as well. // // On the other hand, rent fees are distributed under slightly different philosophy, while // still being stake-weighted. // Ref: distribute_rent_to_validators pub(super) fn distribute_transaction_fees(&self) { let collector_fees = self.collector_fees.load(Relaxed); if collector_fees != 0 { let (deposit, mut burn) = self.fee_rate_governor.burn(collector_fees); if deposit > 0 { let validate_fee_collector = self.validate_fee_collector_account(); match self.deposit_fees( &self.collector_id, deposit, DepositFeeOptions { check_account_owner: validate_fee_collector, check_rent_paying: validate_fee_collector, }, ) { Ok(post_balance) => { self.rewards.write().unwrap().push(( self.collector_id, RewardInfo { reward_type: RewardType::Fee, lamports: deposit as i64, post_balance, commission: None, }, )); } Err(err) => { debug!( "Burned {} lamport tx fee instead of sending to {} due to {}", deposit, self.collector_id, err ); datapoint_warn!( "bank-burned_fee", ("slot", self.slot(), i64), ("num_lamports", deposit, i64), ("error", err.to_string(), String), ); burn += deposit; } } } self.capitalization.fetch_sub(burn, Relaxed); } } // Deposits fees into a specified account and if successful, returns the new balance of that account fn deposit_fees( &self, pubkey: &Pubkey, fees: u64, options: DepositFeeOptions, ) -> Result { let mut account = self.get_account_with_fixed_root(pubkey).unwrap_or_default(); if options.check_account_owner && !system_program::check_id(account.owner()) { return Err(DepositFeeError::InvalidAccountOwner); } let rent = &self.rent_collector().rent; let recipient_pre_rent_state = RentState::from_account(&account, rent); let distribution = account.checked_add_lamports(fees); if distribution.is_err() { return Err(DepositFeeError::LamportOverflow); } if options.check_rent_paying { let recipient_post_rent_state = RentState::from_account(&account, rent); let rent_state_transition_allowed = recipient_post_rent_state.transition_allowed_from(&recipient_pre_rent_state); if !rent_state_transition_allowed { return Err(DepositFeeError::InvalidRentPayingAccount); } } self.store_account(pubkey, &account); Ok(account.lamports()) } // Distribute collected rent fees for this slot to staked validators (excluding stakers) // according to stake. // // The nature of rent fee is the cost of doing business, every validator has to hold (or have // access to) the same list of accounts, so we pay according to stake, which is a rough proxy for // value to the network. // // Currently, rent distribution doesn't consider given validator's uptime at all (this might // change). That's because rent should be rewarded for the storage resource utilization cost. // It's treated differently from transaction fees, which is for the computing resource // utilization cost. // // We can't use collector_id (which is rotated according to stake-weighted leader schedule) // as an approximation to the ideal rent distribution to simplify and avoid this per-slot // computation for the distribution (time: N log N, space: N acct. stores; N = # of // validators). // The reason is that rent fee doesn't need to be incentivized for throughput unlike transaction // fees // // Ref: distribute_transaction_fees #[allow(clippy::needless_collect)] fn distribute_rent_to_validators( &self, vote_accounts: &VoteAccountsHashMap, rent_to_be_distributed: u64, ) { let mut total_staked = 0; // Collect the stake associated with each validator. // Note that a validator may be present in this vector multiple times if it happens to have // more than one staked vote account somehow let mut validator_stakes = vote_accounts .iter() .filter_map(|(_vote_pubkey, (staked, account))| { if *staked == 0 { None } else { total_staked += *staked; Some((account.node_pubkey()?, *staked)) } }) .collect::>(); #[cfg(test)] if validator_stakes.is_empty() { // some tests bank.freezes() with bad staking state self.capitalization .fetch_sub(rent_to_be_distributed, Relaxed); return; } #[cfg(not(test))] assert!(!validator_stakes.is_empty()); // Sort first by stake and then by validator identity pubkey for determinism. // If two items are still equal, their relative order does not matter since // both refer to the same validator. validator_stakes.sort_unstable_by(|(pubkey1, staked1), (pubkey2, staked2)| { (staked1, pubkey1).cmp(&(staked2, pubkey2)).reverse() }); let mut rent_distributed_in_initial_round = 0; let validator_rent_shares = validator_stakes .into_iter() .map(|(pubkey, staked)| { let rent_share = (((staked as u128) * (rent_to_be_distributed as u128)) / (total_staked as u128)) .try_into() .unwrap(); rent_distributed_in_initial_round += rent_share; (pubkey, rent_share) }) .collect::>(); // Leftover lamports after fraction calculation, will be paid to validators starting from highest stake // holder let mut leftover_lamports = rent_to_be_distributed - rent_distributed_in_initial_round; let mut rent_to_burn: u64 = 0; let mut rewards = vec![]; validator_rent_shares .into_iter() .for_each(|(pubkey, rent_share)| { let rent_to_be_paid = if leftover_lamports > 0 { leftover_lamports -= 1; rent_share + 1 } else { rent_share }; if rent_to_be_paid > 0 { let check_account_owner = self.validate_fee_collector_account(); let check_rent_paying = self.prevent_rent_paying_rent_recipients(); match self.deposit_fees( &pubkey, rent_to_be_paid, DepositFeeOptions { check_account_owner, check_rent_paying, }, ) { Ok(post_balance) => { rewards.push(( pubkey, RewardInfo { reward_type: RewardType::Rent, lamports: rent_to_be_paid as i64, post_balance, commission: None, }, )); } Err(err) => { debug!( "Burned {} lamport rent fee instead of sending to {} due to {}", rent_to_be_paid, pubkey, err ); // overflow adding lamports or resulting account is invalid // so burn lamports and track lamports burned per slot rent_to_burn = rent_to_burn.saturating_add(rent_to_be_paid); } } } }); self.rewards.write().unwrap().append(&mut rewards); if rent_to_burn > 0 { self.capitalization.fetch_sub(rent_to_burn, Relaxed); datapoint_warn!( "bank-burned_rent", ("slot", self.slot(), i64), ("num_lamports", rent_to_burn, i64) ); } assert_eq!(leftover_lamports, 0); } pub(super) fn distribute_rent_fees(&self) { let total_rent_collected = self.collected_rent.load(Relaxed); if !self.should_collect_rent() { if total_rent_collected != 0 { warn!("Rent fees collection is disabled, yet total rent collected was non zero! Total rent collected: {total_rent_collected}"); } return; } let (burned_portion, rent_to_be_distributed) = self .rent_collector .rent .calculate_burn(total_rent_collected); debug!( "distributed rent: {} (rounded from: {}, burned: {})", rent_to_be_distributed, total_rent_collected, burned_portion ); self.capitalization.fetch_sub(burned_portion, Relaxed); if rent_to_be_distributed == 0 { return; } self.distribute_rent_to_validators(&self.vote_accounts(), rent_to_be_distributed); } } #[cfg(test)] pub mod tests { use { super::*, crate::genesis_utils::{ create_genesis_config, create_genesis_config_with_leader, create_genesis_config_with_vote_accounts, ValidatorVoteKeypairs, }, solana_sdk::{ account::AccountSharedData, feature_set, native_token::sol_to_lamports, pubkey, rent::Rent, signature::Signer, }, }; #[test] fn test_distribute_transaction_fees() { #[derive(PartialEq)] enum Scenario { Normal, InvalidOwner, RentPaying, } struct TestCase { scenario: Scenario, disable_checks: bool, } impl TestCase { fn new(scenario: Scenario, disable_checks: bool) -> Self { Self { scenario, disable_checks, } } } for test_case in [ TestCase::new(Scenario::Normal, false), TestCase::new(Scenario::Normal, true), TestCase::new(Scenario::InvalidOwner, false), TestCase::new(Scenario::InvalidOwner, true), TestCase::new(Scenario::RentPaying, false), TestCase::new(Scenario::RentPaying, true), ] { let mut genesis = create_genesis_config(0); if test_case.disable_checks { genesis .genesis_config .accounts .remove(&feature_set::validate_fee_collector_account::id()) .unwrap(); } let rent = Rent::default(); let min_rent_exempt_balance = rent.minimum_balance(0); genesis.genesis_config.rent = rent; // Ensure rent is non-zero, as genesis_utils sets Rent::free by default let bank = Bank::new_for_tests(&genesis.genesis_config); let transaction_fees = 100; bank.collector_fees.fetch_add(transaction_fees, Relaxed); assert_eq!(transaction_fees, bank.collector_fees.load(Relaxed)); let (expected_collected_fees, burn_amount) = bank.fee_rate_governor.burn(transaction_fees); assert!(burn_amount > 0); if test_case.scenario == Scenario::RentPaying { // ensure that account balance + collected fees will make it rent-paying let initial_balance = 100; let account = AccountSharedData::new(initial_balance, 0, &system_program::id()); bank.store_account(bank.collector_id(), &account); assert!(initial_balance + transaction_fees < min_rent_exempt_balance); } else if test_case.scenario == Scenario::InvalidOwner { // ensure that account owner is invalid and fee distribution will fail let account = AccountSharedData::new(min_rent_exempt_balance, 0, &Pubkey::new_unique()); bank.store_account(bank.collector_id(), &account); } else { let account = AccountSharedData::new(min_rent_exempt_balance, 0, &system_program::id()); bank.store_account(bank.collector_id(), &account); } let initial_capitalization = bank.capitalization(); let initial_collector_id_balance = bank.get_balance(bank.collector_id()); bank.distribute_transaction_fees(); let new_collector_id_balance = bank.get_balance(bank.collector_id()); if test_case.scenario != Scenario::Normal && !test_case.disable_checks { assert_eq!(initial_collector_id_balance, new_collector_id_balance); assert_eq!( initial_capitalization - transaction_fees, bank.capitalization() ); let locked_rewards = bank.rewards.read().unwrap(); assert!( locked_rewards.is_empty(), "There should be no rewards distributed" ); } else { assert_eq!( initial_collector_id_balance + expected_collected_fees, new_collector_id_balance ); assert_eq!(initial_capitalization - burn_amount, bank.capitalization()); let locked_rewards = bank.rewards.read().unwrap(); assert_eq!( locked_rewards.len(), 1, "There should be one reward distributed" ); let reward_info = &locked_rewards[0]; assert_eq!( reward_info.1.lamports, expected_collected_fees as i64, "The reward amount should match the expected deposit" ); assert_eq!( reward_info.1.reward_type, RewardType::Fee, "The reward type should be Fee" ); } } } #[test] fn test_distribute_transaction_fees_zero() { let genesis = create_genesis_config(0); let bank = Bank::new_for_tests(&genesis.genesis_config); assert_eq!(bank.collector_fees.load(Relaxed), 0); let initial_capitalization = bank.capitalization(); let initial_collector_id_balance = bank.get_balance(bank.collector_id()); bank.distribute_transaction_fees(); let new_collector_id_balance = bank.get_balance(bank.collector_id()); assert_eq!(initial_collector_id_balance, new_collector_id_balance); assert_eq!(initial_capitalization, bank.capitalization()); let locked_rewards = bank.rewards.read().unwrap(); assert!( locked_rewards.is_empty(), "There should be no rewards distributed" ); } #[test] fn test_distribute_transaction_fees_burn_all() { let mut genesis = create_genesis_config(0); genesis.genesis_config.fee_rate_governor.burn_percent = 100; let bank = Bank::new_for_tests(&genesis.genesis_config); let transaction_fees = 100; bank.collector_fees.fetch_add(transaction_fees, Relaxed); assert_eq!(transaction_fees, bank.collector_fees.load(Relaxed)); let initial_capitalization = bank.capitalization(); let initial_collector_id_balance = bank.get_balance(bank.collector_id()); bank.distribute_transaction_fees(); let new_collector_id_balance = bank.get_balance(bank.collector_id()); assert_eq!(initial_collector_id_balance, new_collector_id_balance); assert_eq!( initial_capitalization - transaction_fees, bank.capitalization() ); let locked_rewards = bank.rewards.read().unwrap(); assert!( locked_rewards.is_empty(), "There should be no rewards distributed" ); } #[test] fn test_distribute_transaction_fees_overflow_failure() { let genesis = create_genesis_config(0); let bank = Bank::new_for_tests(&genesis.genesis_config); let transaction_fees = 100; bank.collector_fees.fetch_add(transaction_fees, Relaxed); assert_eq!(transaction_fees, bank.collector_fees.load(Relaxed)); // ensure that account balance will overflow and fee distribution will fail let account = AccountSharedData::new(u64::MAX, 0, &system_program::id()); bank.store_account(bank.collector_id(), &account); let initial_capitalization = bank.capitalization(); let initial_collector_id_balance = bank.get_balance(bank.collector_id()); bank.distribute_transaction_fees(); let new_collector_id_balance = bank.get_balance(bank.collector_id()); assert_eq!(initial_collector_id_balance, new_collector_id_balance); assert_eq!( initial_capitalization - transaction_fees, bank.capitalization() ); let locked_rewards = bank.rewards.read().unwrap(); assert!( locked_rewards.is_empty(), "There should be no rewards distributed" ); } #[test] fn test_deposit_fees() { let initial_balance = 1_000_000_000; let genesis = create_genesis_config(initial_balance); let bank = Bank::new_for_tests(&genesis.genesis_config); let pubkey = genesis.mint_keypair.pubkey(); let deposit_amount = 500; let options = DepositFeeOptions { check_account_owner: true, check_rent_paying: true, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Ok(initial_balance + deposit_amount), "New balance should be the sum of the initial balance and deposit amount" ); } #[test] fn test_deposit_fees_with_overflow() { let initial_balance = u64::MAX; let genesis = create_genesis_config(initial_balance); let bank = Bank::new_for_tests(&genesis.genesis_config); let pubkey = genesis.mint_keypair.pubkey(); let deposit_amount = 500; let options = DepositFeeOptions { check_account_owner: false, check_rent_paying: false, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Err(DepositFeeError::LamportOverflow), "Expected an error due to lamport overflow" ); } #[test] fn test_deposit_fees_invalid_account_owner() { let initial_balance = 1000; let genesis = create_genesis_config_with_leader(0, &pubkey::new_rand(), initial_balance); let bank = Bank::new_for_tests(&genesis.genesis_config); let pubkey = genesis.voting_keypair.pubkey(); let deposit_amount = 500; // enable check_account_owner { let options = DepositFeeOptions { check_account_owner: true, // Intentionally checking for account owner check_rent_paying: false, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Err(DepositFeeError::InvalidAccountOwner), "Expected an error due to invalid account owner" ); } // disable check_account_owner { let options = DepositFeeOptions { check_account_owner: false, check_rent_paying: false, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Ok(initial_balance + deposit_amount), "New balance should be the sum of the initial balance and deposit amount" ); } } #[test] fn test_deposit_fees_invalid_rent_paying() { let initial_balance = 0; let genesis = create_genesis_config(initial_balance); let pubkey = genesis.mint_keypair.pubkey(); let mut genesis_config = genesis.genesis_config; genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default let bank = Bank::new_for_tests(&genesis_config); let min_rent_exempt_balance = genesis_config.rent.minimum_balance(0); let deposit_amount = 500; assert!(initial_balance + deposit_amount < min_rent_exempt_balance); // enable check_rent_paying { let options = DepositFeeOptions { check_account_owner: false, check_rent_paying: true, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Err(DepositFeeError::InvalidRentPayingAccount), "Expected an error due to invalid rent paying account" ); } // disable check_rent_paying { let options = DepositFeeOptions { check_account_owner: false, check_rent_paying: false, }; assert_eq!( bank.deposit_fees(&pubkey, deposit_amount, options), Ok(initial_balance + deposit_amount), "New balance should be the sum of the initial balance and deposit amount" ); } } #[test] fn test_distribute_rent_to_validators_rent_paying() { solana_logger::setup(); const RENT_PER_VALIDATOR: u64 = 55; const TOTAL_RENT: u64 = RENT_PER_VALIDATOR * 4; let empty_validator = ValidatorVoteKeypairs::new_rand(); let rent_paying_validator = ValidatorVoteKeypairs::new_rand(); let becomes_rent_exempt_validator = ValidatorVoteKeypairs::new_rand(); let rent_exempt_validator = ValidatorVoteKeypairs::new_rand(); let keypairs = vec![ &empty_validator, &rent_paying_validator, &becomes_rent_exempt_validator, &rent_exempt_validator, ]; let genesis_config_info = create_genesis_config_with_vote_accounts( sol_to_lamports(1000.), &keypairs, vec![sol_to_lamports(1000.); 4], ); let mut genesis_config = genesis_config_info.genesis_config; genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default for deactivate_feature in [false, true] { if deactivate_feature { genesis_config .accounts .remove(&feature_set::prevent_rent_paying_rent_recipients::id()) .unwrap(); } let bank = Bank::new_for_tests(&genesis_config); let rent = &bank.rent_collector().rent; let rent_exempt_minimum = rent.minimum_balance(0); // Make one validator have an empty identity account let mut empty_validator_account = bank .get_account_with_fixed_root(&empty_validator.node_keypair.pubkey()) .unwrap(); empty_validator_account.set_lamports(0); bank.store_account( &empty_validator.node_keypair.pubkey(), &empty_validator_account, ); // Make one validator almost rent-exempt, less RENT_PER_VALIDATOR let mut becomes_rent_exempt_validator_account = bank .get_account_with_fixed_root(&becomes_rent_exempt_validator.node_keypair.pubkey()) .unwrap(); becomes_rent_exempt_validator_account .set_lamports(rent_exempt_minimum - RENT_PER_VALIDATOR); bank.store_account( &becomes_rent_exempt_validator.node_keypair.pubkey(), &becomes_rent_exempt_validator_account, ); // Make one validator rent-exempt let mut rent_exempt_validator_account = bank .get_account_with_fixed_root(&rent_exempt_validator.node_keypair.pubkey()) .unwrap(); rent_exempt_validator_account.set_lamports(rent_exempt_minimum); bank.store_account( &rent_exempt_validator.node_keypair.pubkey(), &rent_exempt_validator_account, ); let get_rent_state = |bank: &Bank, address: &Pubkey| -> RentState { let account = bank .get_account_with_fixed_root(address) .unwrap_or_default(); RentState::from_account(&account, rent) }; // Assert starting RentStates assert_eq!( get_rent_state(&bank, &empty_validator.node_keypair.pubkey()), RentState::Uninitialized ); assert_eq!( get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()), RentState::RentPaying { lamports: 42, data_size: 0, } ); assert_eq!( get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()), RentState::RentPaying { lamports: rent_exempt_minimum - RENT_PER_VALIDATOR, data_size: 0, } ); assert_eq!( get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()), RentState::RentExempt ); let old_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey()); let old_rent_paying_validator_lamports = bank.get_balance(&rent_paying_validator.node_keypair.pubkey()); let old_becomes_rent_exempt_validator_lamports = bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey()); let old_rent_exempt_validator_lamports = bank.get_balance(&rent_exempt_validator.node_keypair.pubkey()); bank.distribute_rent_to_validators(&bank.vote_accounts(), TOTAL_RENT); let new_empty_validator_lamports = bank.get_balance(&empty_validator.node_keypair.pubkey()); let new_rent_paying_validator_lamports = bank.get_balance(&rent_paying_validator.node_keypair.pubkey()); let new_becomes_rent_exempt_validator_lamports = bank.get_balance(&becomes_rent_exempt_validator.node_keypair.pubkey()); let new_rent_exempt_validator_lamports = bank.get_balance(&rent_exempt_validator.node_keypair.pubkey()); // Assert ending balances; rent should be withheld if test is active and ending RentState // is RentPaying, ie. empty_validator and rent_paying_validator assert_eq!( if deactivate_feature { old_empty_validator_lamports + RENT_PER_VALIDATOR } else { old_empty_validator_lamports }, new_empty_validator_lamports ); assert_eq!( if deactivate_feature { old_rent_paying_validator_lamports + RENT_PER_VALIDATOR } else { old_rent_paying_validator_lamports }, new_rent_paying_validator_lamports ); assert_eq!( old_becomes_rent_exempt_validator_lamports + RENT_PER_VALIDATOR, new_becomes_rent_exempt_validator_lamports ); assert_eq!( old_rent_exempt_validator_lamports + RENT_PER_VALIDATOR, new_rent_exempt_validator_lamports ); // Assert ending RentStates assert_eq!( if deactivate_feature { RentState::RentPaying { lamports: RENT_PER_VALIDATOR, data_size: 0, } } else { RentState::Uninitialized }, get_rent_state(&bank, &empty_validator.node_keypair.pubkey()), ); assert_eq!( if deactivate_feature { RentState::RentPaying { lamports: old_rent_paying_validator_lamports + RENT_PER_VALIDATOR, data_size: 0, } } else { RentState::RentPaying { lamports: old_rent_paying_validator_lamports, data_size: 0, } }, get_rent_state(&bank, &rent_paying_validator.node_keypair.pubkey()), ); assert_eq!( RentState::RentExempt, get_rent_state(&bank, &becomes_rent_exempt_validator.node_keypair.pubkey()), ); assert_eq!( RentState::RentExempt, get_rent_state(&bank, &rent_exempt_validator.node_keypair.pubkey()), ); } } #[test] fn test_distribute_rent_to_validators_invalid_owner() { struct TestCase { disable_owner_check: bool, use_invalid_owner: bool, } impl TestCase { fn new(disable_owner_check: bool, use_invalid_owner: bool) -> Self { Self { disable_owner_check, use_invalid_owner, } } } for test_case in [ TestCase::new(false, false), TestCase::new(false, true), TestCase::new(true, false), TestCase::new(true, true), ] { let genesis_config_info = create_genesis_config_with_leader(0, &Pubkey::new_unique(), 100); let mut genesis_config = genesis_config_info.genesis_config; genesis_config.rent = Rent::default(); // Ensure rent is non-zero, as genesis_utils sets Rent::free by default if test_case.disable_owner_check { genesis_config .accounts .remove(&feature_set::validate_fee_collector_account::id()) .unwrap(); } let bank = Bank::new_for_tests(&genesis_config); let initial_balance = 1_000_000; let account_owner = if test_case.use_invalid_owner { Pubkey::new_unique() } else { system_program::id() }; let account = AccountSharedData::new(initial_balance, 0, &account_owner); bank.store_account(bank.collector_id(), &account); let initial_capitalization = bank.capitalization(); let rent_fees = 100; bank.distribute_rent_to_validators(&bank.vote_accounts(), rent_fees); let new_capitalization = bank.capitalization(); let new_balance = bank.get_balance(bank.collector_id()); if test_case.use_invalid_owner && !test_case.disable_owner_check { assert_eq!(initial_balance, new_balance); assert_eq!(initial_capitalization - rent_fees, new_capitalization); assert_eq!(bank.rewards.read().unwrap().len(), 0); } else { assert_eq!(initial_balance + rent_fees, new_balance); assert_eq!(initial_capitalization, new_capitalization); assert_eq!(bank.rewards.read().unwrap().len(), 1); } } } }