diff --git a/substrate-node/pallets/pallet-smart-contract/src/billing.rs b/substrate-node/pallets/pallet-smart-contract/src/billing.rs index 3b1ef6b17..a4a45a106 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/billing.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/billing.rs @@ -4,8 +4,7 @@ use frame_support::{ dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo}, ensure, traits::{ - fungible::Inspect, - tokens::{Fortitude::Polite, Preservation::Preserve}, + tokens::{fungible::*, Fortitude::Polite, Preservation::Preserve}, LockableCurrency, OnUnbalanced, ReservableCurrency, }, }; @@ -72,17 +71,20 @@ impl Pallet { } fn should_bill_contract(contract: &types::Contract) -> bool { - if let types::ContractData::NodeContract(node_contract) = contract.contract_type.clone() { - let bill_ip = node_contract.public_ips > 0; - let bill_cu_su = !NodeContractResources::::get(contract.contract_id) - .used - .is_empty(); - let bill_nu = - ContractBillingInformationByID::::get(contract.contract_id).amount_unbilled > 0; - - return bill_ip || bill_cu_su || bill_nu; + match &contract.contract_type { + types::ContractData::NodeContract(node_contract) => { + let bill_ip = node_contract.public_ips > 0; + let bill_cu_su = !NodeContractResources::::get(contract.contract_id) + .used + .is_empty(); + let bill_nu = ContractBillingInformationByID::::get(contract.contract_id) + .amount_unbilled + > 0; + + return bill_ip || bill_cu_su || bill_nu; + } + _ => true, } - true } pub fn bill_contract_using_signed_transaction(contract_id: u64) -> Result<(), Error> { @@ -121,7 +123,7 @@ impl Pallet { pub fn bill_contract(contract_id: u64) -> DispatchResultWithPostInfo { log::debug!("Starting billing for contract_id: {:?}", contract_id); - // pre check if contract is already processed in this block + // Check if contract is already processed in this block let mut seen_contracts = SeenContracts::::get(); ensure!( !seen_contracts.contains(&contract_id), @@ -143,30 +145,35 @@ impl Pallet { Error::::PricingPolicyNotExists })?; - // if contract is not name contract ensure the node, farm and farmer twin exists - let farmer_twin = if !matches!(contract.contract_type, types::ContractData::NameContract(_)) - { - let node = pallet_tfgrid::Nodes::::get(contract.get_node_id()).ok_or_else(|| { - log::error!("Node not exists for contract_id: {:?}", contract_id); - Error::::NodeNotExists - })?; - let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or_else(|| { - log::error!("Farm not exists for node_id: {:?}", node.farm_id); - Error::::FarmNotExists - })?; - let farmer_twin = pallet_tfgrid::Twins::::get(farm.twin_id).ok_or_else(|| { - log::error!("Twin not exists for farm_id: {:?}", farm.twin_id); - Error::::TwinNotExists - })?; - Some(farmer_twin) - } else { - None - }; + // Check if contract is not a name contract ensure the node, farm and farmer twin exists + let (farmer_twin, node_certification) = + if !matches!(contract.contract_type, types::ContractData::NameContract(_)) { + let node = + pallet_tfgrid::Nodes::::get(contract.get_node_id()).ok_or_else(|| { + log::error!("Node not exists for contract_id: {:?}", contract_id); + Error::::NodeNotExists + })?; + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or_else(|| { + log::error!("Farm not exists for node_id: {:?}", node.farm_id); + Error::::FarmNotExists + })?; + let farmer_twin = + pallet_tfgrid::Twins::::get(farm.twin_id).ok_or_else(|| { + log::error!("Twin not exists for farm_id: {:?}", farm.twin_id); + Error::::TwinNotExists + })?; + (Some(farmer_twin), Some(node.certification)) + } else { + (None, None) + }; - // Update contract payment state and contract lock + // Switch lazily from old contract lock to use new contract payment state + // This allows tracking the overdrafted amount for the current contract + // While still avoiding the need for a storage migration for all contracts let mut contract_payment_state = ContractPaymentState::::get(contract.contract_id); - let old_contract_lock = ContractLock::::take(contract.contract_id); // get and remove the contract lock - // this is no-op if the contract lock is empty (migrated) + // Get and remove the contract lock from storage + let old_contract_lock = ContractLock::::take(contract.contract_id); + // This is no-op if the contract lock is empty (migrated) Self::ensure_contract_migrated( &src_twin.account_id, &old_contract_lock, @@ -197,9 +204,9 @@ impl Pallet { Self::deposit_event(Event::RentWaived { contract_id: contract.contract_id, }); - // although no billing required here, in deleted state, we should continue the billing processs for conatrcts to distribute rewards if any and clean up storage + // Although no billing required here, in deleted state, we should continue the billing process for contracts to distribute rewards if any and clean up storage if matches!(contract.state, types::ContractState::Created) { - // so for created rent contracts, if the node is in standby, don't expect the billing cycle to advance + // So for created rent contracts, if the node is in standby, don't expect the billing cycle to advance return Ok(().into()); } } @@ -209,7 +216,11 @@ impl Pallet { (BalanceOf::::zero(), types::DiscountLevel::None) } else { contract - .calculate_contract_cost_tft(total_usable_balance, seconds_elapsed) + .calculate_contract_cost_tft( + total_usable_balance, + seconds_elapsed, + node_certification, + ) .map_err(|e| { log::error!("Error while calculating contract cost: {:?}", e); e @@ -229,7 +240,7 @@ impl Pallet { let total_amount_due = standard_amount_due.defensive_saturating_add(additional_amount_due); - // if the amount due is zero and the contract is not in deleted, don't bill the contract (mostly node contarct on a rented node) + // If the amount due is zero and the contract is not in deleted state, don't bill the contract (mostly node contarct on a rented node) if total_amount_due.is_zero() && !matches!(contract.state, types::ContractState::Deleted(_)) { log::debug!( @@ -256,7 +267,7 @@ impl Pallet { _ = Self::handle_grace(&mut contract, has_sufficient_fund, current_block); if has_sufficient_fund { - log::info!("Billing contract_id: {:?}, This cycle amount due: {:?}, Total (include previous overdraft) {:?}", contract.contract_id, total_amount_due, total_amount_to_reserve); + log::debug!("Billing contract_id: {:?}, This cycle amount due: {:?}, Total (include previous overdraft) {:?}", contract.contract_id, total_amount_due, total_amount_to_reserve); Self::reserve_funds( &mut contract_payment_state, standard_amount_due, @@ -268,10 +279,6 @@ impl Pallet { discount_received, )?; } else { - log::info!( - "Contract payment overdrafted for contract_id: {:?}", - contract.contract_id - ); Self::overdraft_funds( &mut contract_payment_state, standard_amount_due, @@ -280,9 +287,15 @@ impl Pallet { &contract, now, )?; + log::debug!( + "Contract payment overdrafted for contract_id: {:?}, Contract state: {:?}, Current Overdrafted amount: {:?}", + contract.contract_id, + contract.state, + contract_payment_state.get_overdrafted() + ); } - // distribute rewards, no-op if contract nither in deleted state nor the distribution frequency is reached + // Distribute rewards Self::remit_funds( &contract, &mut contract_payment_state, @@ -290,12 +303,8 @@ impl Pallet { farmer_twin, &pricing_policy, )?; - log::info!( - "Rewards distributed for contract_id: {:?}", - contract.contract_id - ); - // housekeeping for contracts in deleted state + // Housekeeping for contracts in deleted state if matches!(contract.state, types::ContractState::Deleted(_)) { log::info!( "contract id {:?} in deleted state. clean up storage.", @@ -304,7 +313,7 @@ impl Pallet { return Self::remove_contract(contract.contract_id); } - // reset NU amount if the contract is a node contract + // Reset NU amount if the contract is a node contract if matches!(contract.contract_type, types::ContractData::NodeContract(_)) { let mut contract_billing_info = ContractBillingInformationByID::::get(contract.contract_id); @@ -343,13 +352,14 @@ impl Pallet { } } - // Handles the grace period for a contract + // Handles the transition between different contract states based on the fund availability + // May emits one of ContractGracePeriodStarted, ContractGracePeriodEnded, ContractGracePeriodElapsed events fn handle_grace( contract: &mut types::Contract, has_sufficient_fund: bool, current_block: u64, ) -> DispatchResultWithPostInfo { - Ok((match contract.state { + match contract.state { types::ContractState::GracePeriod(_) if has_sufficient_fund => { log::info!("Contract {:?} is in grace period, but balance is recharged, moving to created state at block {:?}", contract.contract_id, current_block); Self::update_contract_state(contract, &types::ContractState::Created)?; @@ -366,7 +376,7 @@ impl Pallet { log::info!("Contract {:?} state changed to deleted at block {:?} due to an expired grace period. Elapsed blocks: {:?}", contract.contract_id, current_block, diff); Self::deposit_event(Event::ContractGracePeriodElapsed { contract_id: contract.contract_id, - grace_period: diff + grace_period: diff, }); Self::update_contract_state( contract, @@ -395,10 +405,12 @@ impl Pallet { )?; } _ => (), - }).into()) + } + Ok(().into()) } - // + // Holding funds from a user's account to guarantee that they are available later. + // Emits ContractBilled event fn reserve_funds( contract_payment_state: &mut types::ContractPaymentState>, standard_amount_due: BalanceOf, @@ -428,6 +440,8 @@ impl Pallet { Ok(().into()) } + // Increasing the overdraft in the user's account + // Emits ContractPaymentOverdrafted event fn overdraft_funds( contract_payment_state: &mut types::ContractPaymentState>, standard_amount_due: BalanceOf, @@ -449,14 +463,15 @@ impl Pallet { contract_id: contract.contract_id, timestamp: now, partial_billed_amount: reservable, - // this is the total overdrafted amount for this contract since grace period started not per cycle + // This is the total overdrafted amount for this contract since grace period started overdrafted_amount: contract_payment_state.get_overdrafted(), }); Ok(().into()) } - // orcastrate the distribution of rewards - // emits RewardDistributed event + // Orcastrate the distribution of rewards + // Emits RewardDistributed event + // No-Op if contract nither in deleted state nor the distribution frequency is reached fn remit_funds( contract: &types::Contract, contract_payment_state: &mut types::ContractPaymentState>, @@ -466,9 +481,8 @@ impl Pallet { ) -> DispatchResultWithPostInfo { let is_deleted = matches!(contract.state, types::ContractState::Deleted(_)); let should_distribute_rewards = - (contract_payment_state.cycles >= T::DistributionFrequency::get() || is_deleted) - && contract_payment_state.has_reserved_amount(); - if should_distribute_rewards { + contract_payment_state.cycles >= T::DistributionFrequency::get() || is_deleted; + if should_distribute_rewards && contract_payment_state.has_reserved_amount() { let standard_rewards = contract_payment_state.standard_reserved; let additional_rewards = contract_payment_state.additional_reserved; // distribute additional rewards to the farm twin @@ -508,16 +522,27 @@ impl Pallet { contract_payment_state.standard_reserved = BalanceOf::::zero(); contract_payment_state.cycles = 0; + log::info!( + "Rewards distributed for contract_id: {:?}", + contract.contract_id + ); Self::deposit_event(Event::RewardDistributed { contract_id: contract.contract_id, standard_rewards, additional_rewards: distributed_additional_amount, }); + } else { + log::debug!( + "Not distributing rewards for contract_id: {:?}, cycles: {:?}, reserved amount: {:?}", + contract.contract_id, + contract_payment_state.cycles, + contract_payment_state.get_reserved() + ); } Ok(().into()) } - // transferring the held or reserved funds from the user's account to the beneficiaries (foundation, staking pool, solution providers, sales account) and burning the remainder + // Transferring the held or reserved funds from the user's account to the beneficiaries (foundation, staking pool, solution providers, sales account) and burning the remainder fn distribute_standard_rewards( src_twin: &pallet_tfgrid::types::Twin, contract_id: u64, @@ -612,7 +637,7 @@ impl Pallet { let amount_to_burn = amount.defensive_saturating_sub(total_distributed); let (to_burn, reminder) = T::Currency::slash_reserved(&src_twin.account_id, amount_to_burn); if !reminder.is_zero() { - // assertion + // Shouldn't happen log::warn!( "Failed to burn the whole amount: want {:?}, remainder {:?}", amount_to_burn, @@ -632,6 +657,8 @@ impl Pallet { Ok(().into()) } + // Wrapper around the balances::repatriate_reserved function to handle reserved funds + // As much funds up to value will be deducted as possible. If this is less than amount, then the reminder amount will be returned. fn transfer_reserved( src_account: &T::AccountId, dst_account: &T::AccountId, @@ -658,6 +685,7 @@ impl Pallet { } reminder } + // Shouldn't happen, uless the destination account is not able to receive the funds Err(e) => { log::error!("Error while repatriating reserved balance: {:?}. source: {:?}, destnation: {:?}", e, src_account, dst_account); amount @@ -665,7 +693,7 @@ impl Pallet { } } - // handling rent contracts, associated node contracts are also transitioned to the appropriate state (either Created or GracePeriod). + // Handling rent contracts, associated node contracts are also transitioned to the appropriate state (either Created or GracePeriod). fn handle_grace_rent_contract( contract: &mut types::Contract, state: types::ContractState, @@ -772,8 +800,7 @@ impl Pallet { // Get the usable balance of an account // This is the balance minus the minimum balance (spendable = free - max(frozen - on_hold, ED)) pub fn get_usable_balance(account_id: &T::AccountId) -> BalanceOf { - let spendable = - pallet_balances::pallet::Pallet::::reducible_balance(account_id, Preserve, Polite); + let spendable = ::Currency::reducible_balance(account_id, Preserve, Polite); let b = spendable.saturated_into::(); BalanceOf::::saturated_from(b) } @@ -787,19 +814,17 @@ impl Pallet { } // Get the reservable balance of an account - // This is the balance minus the minimum balance (reservable = free - ED - Frozen ) + // This is the balance minus the minimum balance (reservable = free - ED - Frozen) fn get_reservable_balance(account_id: &T::AccountId) -> BalanceOf { let account = T::AccountStore::get(account_id); let free = account.free; let frozen = account.frozen; - + let minimum_balance = ::Currency::minimum_balance().saturated_into::(); // Get the reservable balance let reservable = free - .saturating_sub( - as frame_support::traits::fungible::Inspect< - T::AccountId, - >>::minimum_balance(), - ) + .saturating_sub(::Balance::saturated_from( + minimum_balance, + )) .saturating_sub(frozen); BalanceOf::::saturated_from(reservable.saturated_into::()) } diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index ce0875c5f..69a4b7e39 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -9,6 +9,8 @@ use tfchain_support::{ types::NodeCertification, }; +pub const CERTIFIED_INCREASE_FACTOR: f64 = 1.25; + impl types::Contract { pub fn get_billing_info(&self) -> types::ContractBillingInformation { pallet::ContractBillingInformationByID::::get(self.contract_id) @@ -18,10 +20,10 @@ impl types::Contract { &self, balance: BalanceOf, seconds_elapsed: u64, + certification_type: Option, ) -> Result<(BalanceOf, types::DiscountLevel), DispatchErrorWithPostInfo> { // Fetch the default pricing policy and certification type let pricing_policy = pallet_tfgrid::PricingPolicies::::get(1).unwrap(); - let certification_type = NodeCertification::Diy; // Calculate the cost for a contract, can be any of: // - NodeContract @@ -30,7 +32,6 @@ impl types::Contract { let total_cost = self.calculate_contract_cost_units_usd(&pricing_policy, seconds_elapsed)?; - // If cost is 0, reinsert to be billed at next interval if total_cost == 0 { return Ok((BalanceOf::::zero(), types::DiscountLevel::None)); } @@ -114,11 +115,7 @@ impl types::Contract { } // Calculates the cost of extra fee for a dedicated node in TFT. - pub fn calculate_extra_fee_cost_tft( - &self, - node_id: u32, - seconds_elapsed: u64, - ) -> BalanceOf { + pub fn calculate_extra_fee_cost_tft(&self, node_id: u32, seconds_elapsed: u64) -> BalanceOf { let cost = calculate_extra_fee_cost_units_usd::(node_id, seconds_elapsed); if cost == 0 { return BalanceOf::::zero(); @@ -297,7 +294,7 @@ pub fn calculate_discount_tft( amount_due: u64, seconds_elapsed: u64, balance: BalanceOf, - certification_type: NodeCertification, + certification_type: Option, ) -> (BalanceOf, types::DiscountLevel) { if amount_due == 0 { return (BalanceOf::::zero(), types::DiscountLevel::None); @@ -329,8 +326,9 @@ pub fn calculate_discount_tft( let mut amount_due = U64F64::from_num(amount_due) * discount_received.price_multiplier(); // Certified capacity costs 25% more - if certification_type == NodeCertification::Certified { - amount_due = amount_due * U64F64::from_num(1.25); + if let Some(NodeCertification::Certified) = certification_type { + log::debug!("Certified node detected, increasing amount due by 25%"); + amount_due *= U64F64::from_num(CERTIFIED_INCREASE_FACTOR); } // convert to balance object @@ -340,9 +338,7 @@ pub fn calculate_discount_tft( (amount_due, discount_received) } -pub fn calculate_cost_in_tft_from_units_usd( - cost_units_usd: u64, -) -> u64 { +pub fn calculate_cost_in_tft_from_units_usd(cost_units_usd: u64) -> u64 { let avg_tft_price = pallet_tft_price::AverageTftPrice::::get(); // Guarantee tft price will never be lower than min tft price @@ -360,5 +356,7 @@ pub fn calculate_cost_in_tft_from_units_usd( let cost_tft = U64F64::from_num(cost_units_usd) / U64F64::from_num(tft_price_units_usd); // Multiply by the chain precision (7 decimals) - (cost_tft * U64F64::from_num(10u64.pow(7))).round().to_num::() + (cost_tft * U64F64::from_num(10u64.pow(7))) + .round() + .to_num::() } diff --git a/substrate-node/pallets/pallet-smart-contract/src/lib.rs b/substrate-node/pallets/pallet-smart-contract/src/lib.rs index 5d32653db..f1c6e2b34 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -63,7 +63,7 @@ pub mod pallet { use super::*; use frame_support::{ pallet_prelude::*, - traits::{Currency, Get, Hooks, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency, NamedReservableCurrency, tokens::fungible::*}, + traits::{Currency, Get, Hooks, LockIdentifier, LockableCurrency, OnUnbalanced, ReservableCurrency, tokens::fungible::*}, }; use frame_system::{ self as system, ensure_signed, @@ -216,7 +216,7 @@ pub mod pallet { + pallet_session::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; - type Currency: LockableCurrency + ReservableCurrency + InspectHold; + type Currency: LockableCurrency + ReservableCurrency + InspectHold + Inspect; /// Handler for the unbalanced decrement when slashing (burning collateral) type Burn: OnUnbalanced>; type StakingPoolAccount: Get; diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index b73c32a9a..94ae7c288 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -1852,7 +1852,7 @@ fn test_name_contract_billing() { let balance = Balances::free_balance(&twin.account_id); let second_elapsed = BillingFrequency::get() * SECS_PER_BLOCK; let (contract_cost, discount_level) = contract - .calculate_contract_cost_tft(balance, second_elapsed) + .calculate_contract_cost_tft(balance, second_elapsed, None) .unwrap(); // the contractbill event should look like: @@ -3954,15 +3954,18 @@ fn test_set_dedicated_node_extra_fee_and_create_rent_contract_billing_works() { let mut rent_contract_cost_tft = 0u64; let mut extra_fee_cost_tft = 0; - // advance 24 cycles to reach reward distribution block - for i in 1..=DistributionFrequency::get() as u64 { + // advance 25 cycles to reach reward distribution block + for i in 1..=DistributionFrequency::get() as u64 + 1u64{ let block_number = start_block + i * BillingFrequency::get(); pool_state.write().should_call_bill_contract( rent_contract_id, Ok(Pays::Yes.into()), block_number, ); - run_to_block(block_number, Some(&mut pool_state)); + log::debug!("i {} after pool block_number: {}", i, block_number); + + run_to_block( block_number, Some(&mut pool_state)); + log::debug!("i {} after run block_number: {}", i, block_number); // check why aggregating seconds elapsed is giving different results let elapsed_time_in_secs = BillingFrequency::get() * SECS_PER_BLOCK; @@ -3970,7 +3973,7 @@ fn test_set_dedicated_node_extra_fee_and_create_rent_contract_billing_works() { // aggregate rent contract cost let free_balance = Balances::free_balance(&twin.account_id); let (contract_cost_tft, _) = rent_contract - .calculate_contract_cost_tft(free_balance, elapsed_time_in_secs) + .calculate_contract_cost_tft(free_balance, elapsed_time_in_secs, None) .unwrap(); rent_contract_cost_tft += contract_cost_tft; @@ -4039,7 +4042,7 @@ macro_rules! test_calculate_discount { amount_due, seconds_elapsed, balance.round().to_num::(), - NodeCertification::Diy, + None, ); assert_eq!( @@ -4205,7 +4208,7 @@ fn calculate_tft_cost(contract_id: u64, twin_id: u32, blocks: u64) -> (u64, type let b = Balances::free_balance(&twin.account_id); let contract = SmartContractModule::contracts(contract_id).unwrap(); let (amount_due, discount_received) = - contract.calculate_contract_cost_tft(b, blocks * 6).unwrap(); + contract.calculate_contract_cost_tft(b, blocks * 6, None).unwrap(); (amount_due, discount_received) } diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index 2dc74c080..1b80e3e55 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -318,13 +318,13 @@ where // Method to return weather the contract has reserved amount or not pub fn has_reserved_amount(&self) -> bool { - self.standard_reserved != BalanceOf::zero() || self.additional_reserved != BalanceOf::zero() + !self.standard_reserved.is_zero() || !self.additional_reserved.is_zero() } // Method to return weather the contract has overdrafted amount or not pub fn has_overdrafted_amount(&self) -> bool { - self.standard_overdrafted != BalanceOf::zero() - || self.additional_overdrafted != BalanceOf::zero() + !self.standard_overdrafted.is_zero() + || !self.additional_overdrafted.is_zero() } // Method to settle partial overdrafted amount @@ -359,7 +359,6 @@ where .defensive_saturating_reduce(remaining_amount); self.standard_reserved .defensive_saturating_accrue(remaining_amount); - remaining_amount = BalanceOf::zero(); } } }