From d06c022f7a77133e0b994b1176afcb8983412fff Mon Sep 17 00:00:00 2001 From: Erwan Renaut <73958772+renauter@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:28:30 -0300 Subject: [PATCH] feat(pallet): allow farmer to set extra fee on its nodes (#726) --- clients/tfchain-client-go/contract_events.go | 7 + clients/tfchain-client-go/contract_test.go | 3 + clients/tfchain-client-go/events.go | 1 + clients/tfchain-client-go/node.go | 55 +++ clients/tfchain-client-go/node_test.go | 23 + clients/tfchain-client-go/utils.go | 5 +- clients/tfchain-client-js/lib/client.js | 14 +- clients/tfchain-client-js/lib/farms.js | 16 +- clients/tfchain-client-js/package.json | 2 +- .../0011-dedicated-nodes-extra-fee.md | 20 + .../pallets/pallet-smart-contract/src/cost.rs | 112 +++-- .../pallets/pallet-smart-contract/src/lib.rs | 266 +++++++++--- .../src/migrations/mod.rs | 4 +- .../src/migrations/types.rs | 48 +++ .../src/migrations/v11.rs | 121 ++++++ .../pallets/pallet-smart-contract/src/mock.rs | 7 +- .../pallet-smart-contract/src/tests.rs | 404 ++++++++++++++++-- .../pallet-smart-contract/src/types.rs | 18 + .../pallets/pallet-tft-price/src/lib.rs | 6 +- substrate-node/runtime/src/lib.rs | 1 + substrate-node/support/src/constants.rs | 5 +- 21 files changed, 991 insertions(+), 147 deletions(-) create mode 100644 docs/architecture/0011-dedicated-nodes-extra-fee.md create mode 100644 substrate-node/pallets/pallet-smart-contract/src/migrations/types.rs create mode 100644 substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs diff --git a/clients/tfchain-client-go/contract_events.go b/clients/tfchain-client-go/contract_events.go index d0c53b110..556f33c9b 100644 --- a/clients/tfchain-client-go/contract_events.go +++ b/clients/tfchain-client-go/contract_events.go @@ -235,3 +235,10 @@ type BillingFrequencyChanged struct { Frequency types.U64 `json:"frequency"` Topics []types.Hash } + +type NodeExtraFeeSet struct { + Phase types.Phase + NodeID types.U32 `json:"node_id"` + ExtraFee types.U64 `json:"extra_fee"` + Topics []types.Hash +} diff --git a/clients/tfchain-client-go/contract_test.go b/clients/tfchain-client-go/contract_test.go index fdadc0503..2775ae8f1 100644 --- a/clients/tfchain-client-go/contract_test.go +++ b/clients/tfchain-client-go/contract_test.go @@ -269,6 +269,9 @@ func TestCreateBatch(t *testing.T) { // third contract should not be created _, err = cl.GetContractIDByNameRegistration(contracts[2].Name) require.Error(t, err) + + err = cl.BatchCancelContract(identity, c) + require.NoError(t, err) }) } diff --git a/clients/tfchain-client-go/events.go b/clients/tfchain-client-go/events.go index decb8b40e..3276c58b2 100644 --- a/clients/tfchain-client-go/events.go +++ b/clients/tfchain-client-go/events.go @@ -409,6 +409,7 @@ type EventRecords struct { SmartContractModule_ServiceContractCanceled []ServiceContractCanceled //nolint:stylecheck,golint SmartContractModule_ServiceContractBilled []ServiceContractBilled //nolint:stylecheck,golint SmartContractModule_BillingFrequencyChanged []BillingFrequencyChanged //nolint:stylecheck,golint + SmartContractModule_NodeExtraFeeSet []NodeExtraFeeSet //nolint:stylecheck,golint // farm events TfgridModule_FarmStored []FarmStored //nolint:stylecheck,golint diff --git a/clients/tfchain-client-go/node.go b/clients/tfchain-client-go/node.go index 0feadbb1b..d01d006af 100644 --- a/clients/tfchain-client-go/node.go +++ b/clients/tfchain-client-go/node.go @@ -714,6 +714,7 @@ func (s *Substrate) SetNodePowerState(identity Identity, up bool) (hash types.Ha return callResponse.Hash, nil } +// GetPowerTarget returns the power target for a node func (s *Substrate) GetPowerTarget(nodeID uint32) (power NodePower, err error) { cl, meta, err := s.GetClient() if err != nil { @@ -750,6 +751,60 @@ func (s *Substrate) GetPowerTarget(nodeID uint32) (power NodePower, err error) { return power, nil } +// SetDedicatedNodePrice sets an extra price on a node expressed in mUSD +// This price will be distributed back to the farmer if the node is rented +// Setting this price also makes the node only available to rent as dedicated +func (s *Substrate) SetDedicatedNodePrice(identity Identity, nodeId uint32, price uint64) (hash types.Hash, err error) { + cl, meta, err := s.GetClient() + if err != nil { + return hash, err + } + + c, err := types.NewCall(meta, "SmartContractModule.set_dedicated_node_extra_fee", nodeId, price) + + if err != nil { + return hash, errors.Wrap(err, "failed to create call") + } + + callResponse, err := s.Call(cl, meta, identity, c) + if err != nil { + return hash, errors.Wrap(err, "failed to update node extra price") + } + + return callResponse.Hash, nil +} + +// GetDedicatedeNodePrice returns the price of a dedicated node +func (s *Substrate) GetDedicatedNodePrice(nodeID uint32) (uint64, error) { + cl, meta, err := s.GetClient() + if err != nil { + return 0, err + } + + bytes, err := Encode(nodeID) + if err != nil { + return 0, errors.Wrap(err, "substrate: encoding error building query arguments") + } + + key, err := types.CreateStorageKey(meta, "SmartContractModule", "DedicatedNodesExtraFee", bytes) + if err != nil { + return 0, errors.Wrap(err, "failed to create substrate query key") + } + + var price types.U64 + + ok, err := cl.RPC.State.GetStorageLatest(key, &price) + if err != nil { + return 0, err + } + + if !ok { + return 0, nil + } + + return uint64(price), nil +} + func (s *Substrate) SetNodeGpuStatus(identity Identity, state bool) (hash types.Hash, err error) { cl, meta, err := s.GetClient() if err != nil { diff --git a/clients/tfchain-client-go/node_test.go b/clients/tfchain-client-go/node_test.go index bbc7d2c5a..8e4136853 100644 --- a/clients/tfchain-client-go/node_test.go +++ b/clients/tfchain-client-go/node_test.go @@ -44,6 +44,29 @@ func TestGetNodes(t *testing.T) { require.Greater(t, len(nodes), 0) } +func TestSetDedicatedNodePrice(t *testing.T) { + var nodeID uint32 + + cl := startLocalConnection(t) + defer cl.Close() + + identity, err := NewIdentityFromSr25519Phrase(BobMnemonics) + require.NoError(t, err) + + farmID, twinID := assertCreateFarm(t, cl) + + nodeID = assertCreateNode(t, cl, farmID, twinID, identity) + + price := 100000000 + _, err = cl.SetDedicatedNodePrice(identity, nodeID, uint64(price)) + require.NoError(t, err) + + priceSet, err := cl.GetDedicatedNodePrice(nodeID) + require.NoError(t, err) + + require.Equal(t, uint64(price), priceSet) +} + func TestUptimeReport(t *testing.T) { cl := startLocalConnection(t) defer cl.Close() diff --git a/clients/tfchain-client-go/utils.go b/clients/tfchain-client-go/utils.go index 42034a796..4b5c6b896 100644 --- a/clients/tfchain-client-go/utils.go +++ b/clients/tfchain-client-go/utils.go @@ -21,7 +21,6 @@ var ( // https://github.com/threefoldtech/tfchain/blob/development/substrate-node/runtime/src/lib.rs#L701 var moduleErrors = [][]string{ nil, // System - nil, // RandomnessCollectiveFlip nil, // Timestamp nil, // Balances nil, // ValidatorSet @@ -44,6 +43,7 @@ var moduleErrors = [][]string{ nil, // Validator nil, // Dao nil, // Utility + nil, // Historical } // https://github.com/threefoldtech/tfchain_pallets/blob/bc9c5d322463aaf735212e428da4ea32b117dc24/pallet-smart-contract/src/lib.rs#L58 @@ -72,7 +72,7 @@ var smartContractModuleErrors = []string{ "MethodIsDeprecated", "NodeHasActiveContracts", "NodeHasRentContract", - "NodeIsNotDedicated", + "FarmIsNotDedicated", "NodeNotAvailableToDeploy", "CannotUpdateContractInGraceState", "NumOverflow", @@ -99,6 +99,7 @@ var smartContractModuleErrors = []string{ "IsNotAnAuthority", "WrongAuthority", "UnauthorizedToChangeSolutionProviderId", + "UnauthorizedToSetExtraFee", } // https://github.com/threefoldtech/tfchain/blob/development/substrate-node/pallets/pallet-smart-contract/src/lib.rs#L321 diff --git a/clients/tfchain-client-js/lib/client.js b/clients/tfchain-client-js/lib/client.js index cea57483b..c70fe6aec 100644 --- a/clients/tfchain-client-js/lib/client.js +++ b/clients/tfchain-client-js/lib/client.js @@ -14,7 +14,7 @@ const { } = require('./twin') const { createFarm, getFarm, deleteFarm, - listFarms, addFarmIP, deleteFarmIP, setNodePower + listFarms, addFarmIP, deleteFarmIP, setNodePower, setDedicatedNodePrice } = require('./farms') const { createNode, updateNode, getNode, @@ -352,6 +352,18 @@ class Client { async getServiceContract(serviceContractId) { return getServiceContract(this, serviceContractId) } + + // setDedicatedNodePrice sets an extra price on a node expressed in mUSD + // This price will be distributed back to the farmer if the node is rented + // Setting this price also makes the node only available to rent as dedicated + async setDedicatedNodePrice(nodeId, price, callback) { + return setDedicatedNodePrice(this, nodeId, price, callback) + } + + // getDedicatedNodePrice returns the extra price on a node expressed in mUSD + async getDedicatedNodePrice(nodeId) { + return getDedicatedNodePrice(this, nodeId) + } } async function getPolkaAPI(url) { if (!url || url === '') { diff --git a/clients/tfchain-client-js/lib/farms.js b/clients/tfchain-client-js/lib/farms.js index 5c88085bb..5af71cd36 100644 --- a/clients/tfchain-client-js/lib/farms.js +++ b/clients/tfchain-client-js/lib/farms.js @@ -119,6 +119,18 @@ async function setNodePower (self, nodeID, power, callback) { .signAndSend(self.key, { nonce }, callback) } +async function setDedicatedNodePrice(self, nodeID, price, callback) { + const nonce = await self.api.rpc.system.accountNextIndex(self.address) + return self.api.tx.smartContractModule + .setDedicatedNodeExtraFee(nodeID, price) + .signAndSend(self.key, { nonce }, callback) +} + +async function getDedicatedNodePrice(self, nodeID) { + const price = await self.api.query.smartContractModule.dedicatedNodesExtraFee(nodeID) + return price.toNumber() +} + async function validateFarm (self, name) { if (name === '') { throw Error('farm should have a name') @@ -132,5 +144,7 @@ module.exports = { listFarms, addFarmIP, deleteFarmIP, - setNodePower + setNodePower, + setDedicatedNodePrice, + getDedicatedNodePrice } diff --git a/clients/tfchain-client-js/package.json b/clients/tfchain-client-js/package.json index 5a115d8b6..597bdd589 100644 --- a/clients/tfchain-client-js/package.json +++ b/clients/tfchain-client-js/package.json @@ -1,6 +1,6 @@ { "name": "tfgrid-api-client", - "version": "1.27.0", + "version": "1.28.0", "description": "API client for the TF Grid", "main": "index.js", "scripts": { diff --git a/docs/architecture/0011-dedicated-nodes-extra-fee.md b/docs/architecture/0011-dedicated-nodes-extra-fee.md new file mode 100644 index 000000000..4546ec290 --- /dev/null +++ b/docs/architecture/0011-dedicated-nodes-extra-fee.md @@ -0,0 +1,20 @@ +# 11. Dedicated nodes extra fee + +Date: 2023-06-06 + +## Status + +Accepted + +## Context + +For supporting GPU on the grid (https://github.com/threefoldtech/home/issues/1392) we are adding an option for a farmer to set an extra fee for his nodes that have a GPU or other special feautures enabled. This fee will be added to the price of the capacity price. The full fee will be paid to the farmer. + +By setting a price on a node, this node becomes dedicated and can only be rented by creating a `RentContract` on that node. This means that when a farmer sets an extra price on a node, this node cannot be shared anymore. + +## Decision + +Added a new storage map `DedicatedNodesExtraFee` on `pallet-smart-contract` which can be set by the owner of that node (the farmer) by calling `set_dedicated_node_extra_fee`. +The input of this call is `node_id` and `fee` which is expressed in mUSD (USD * 1000). This fee is expressed in a fee per month. + +We also changed the `ContractLock` storage to include a new field `extra_amount_locked` which is the total fee that is locked by this contract. \ No newline at end of file diff --git a/substrate-node/pallets/pallet-smart-contract/src/cost.rs b/substrate-node/pallets/pallet-smart-contract/src/cost.rs index bd2675b16..dbd2576b0 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/cost.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/cost.rs @@ -4,12 +4,16 @@ use crate::pallet::Error; use crate::types; use crate::types::{Contract, ContractBillingInformation, ServiceContract, ServiceContractBill}; use crate::Config; +use crate::DedicatedNodesExtraFee; use frame_support::{dispatch::DispatchErrorWithPostInfo, traits::Get}; +use log; use pallet_tfgrid::types as pallet_tfgrid_types; -use sp_runtime::{Percent, SaturatedConversion}; +use sp_runtime::{traits::Zero, Percent, SaturatedConversion}; use substrate_fixed::types::U64F64; use tfchain_support::{ - constants::time::SECS_PER_HOUR, resources::Resources, types::NodeCertification, + constants::time::{SECS_PER_HOUR, SECS_PER_MONTH}, + resources::Resources, + types::NodeCertification, }; impl Contract { @@ -30,16 +34,14 @@ impl Contract { // - NodeContract // - RentContract // - NameContract - let total_cost = self.calculate_contract_cost(&pricing_policy, seconds_elapsed)?; + 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::::saturated_from(0 as u128), - types::DiscountLevel::None, - )); + return Ok((BalanceOf::::zero(), types::DiscountLevel::None)); } - let total_cost_tft_64 = calculate_cost_in_tft_from_musd::(total_cost)?; + let total_cost_tft_64 = calculate_cost_in_tft_from_units_usd::(total_cost)?; // Calculate the amount due and discount received based on the total_cost amount due let (amount_due, discount_received) = calculate_discount::( @@ -49,10 +51,10 @@ impl Contract { certification_type, ); - return Ok((amount_due, discount_received)); + Ok((amount_due, discount_received)) } - pub fn calculate_contract_cost( + pub fn calculate_contract_cost_units_usd( &self, pricing_policy: &pallet_tfgrid_types::PricingPolicy, seconds_elapsed: u64, @@ -62,13 +64,12 @@ impl Contract { types::ContractData::NodeContract(node_contract) => { // Get the contract billing info to view the amount unbilled for NRU (network resource units) let contract_billing_info = self.get_billing_info(); - // Get the node - if !pallet_tfgrid::Nodes::::contains_key(node_contract.node_id) { + // Make sure the node exists + if pallet_tfgrid::Nodes::::get(node_contract.node_id).is_none() { return Err(DispatchErrorWithPostInfo::from(Error::::NodeNotExists)); } // We know the contract is using resources, now calculate the cost for each used resource - let node_contract_resources = pallet::Pallet::::node_contract_resources(self.contract_id); @@ -89,10 +90,8 @@ impl Contract { contract_cost + contract_billing_info.amount_unbilled } types::ContractData::RentContract(rent_contract) => { - if !pallet_tfgrid::Nodes::::contains_key(rent_contract.node_id) { - return Err(DispatchErrorWithPostInfo::from(Error::::NodeNotExists)); - } - let node = pallet_tfgrid::Nodes::::get(rent_contract.node_id).unwrap(); + let node = pallet_tfgrid::Nodes::::get(rent_contract.node_id) + .ok_or(Error::::NodeNotExists)?; let contract_cost = calculate_resources_cost::( node.resources, @@ -109,12 +108,27 @@ impl Contract { let total_cost_u64f64 = (U64F64::from_num(pricing_policy.unique_name.value) / U64F64::from_num(T::BillingReferencePeriod::get())) * U64F64::from_num(seconds_elapsed); - total_cost_u64f64.to_num::() + total_cost_u64f64.round().to_num::() } }; Ok(total_cost) } + + // 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, + ) -> Result, DispatchErrorWithPostInfo> { + let cost = calculate_extra_fee_cost_units_usd::(node_id, seconds_elapsed); + if cost == 0 { + return Ok(BalanceOf::::zero()); + } + let cost_tft = calculate_cost_in_tft_from_units_usd::(cost)?; + + Ok(BalanceOf::::saturated_from(cost_tft)) + } } impl ServiceContract { @@ -122,15 +136,15 @@ impl ServiceContract { &self, service_bill: ServiceContractBill, ) -> Result, DispatchErrorWithPostInfo> { - // Calculate the cost in mUSD for service contract bill - let total_cost = self.calculate_bill_cost::(service_bill); + // Calculate the cost in units usd for service contract bill + let total_cost = self.calculate_bill_cost_units_usd::(service_bill); if total_cost == 0 { - return Ok(BalanceOf::::saturated_from(0 as u128)); + return Ok(BalanceOf::::zero()); } // Calculate the cost in TFT for service contract - let total_cost_tft_64 = calculate_cost_in_tft_from_musd::(total_cost)?; + let total_cost_tft_64 = calculate_cost_in_tft_from_units_usd::(total_cost)?; // convert to balance object let amount_due: BalanceOf = BalanceOf::::saturated_from(total_cost_tft_64); @@ -138,7 +152,10 @@ impl ServiceContract { return Ok(amount_due); } - pub fn calculate_bill_cost(&self, service_bill: ServiceContractBill) -> u64 { + pub fn calculate_bill_cost_units_usd( + &self, + service_bill: ServiceContractBill, + ) -> u64 { // bill user for service usage for elpased usage (window) in seconds let contract_cost = U64F64::from_num(self.base_fee) * U64F64::from_num(service_bill.window) / U64F64::from_num(T::BillingReferencePeriod::get()) @@ -191,8 +208,24 @@ pub fn calculate_resources_cost( total_cost += total_ip_cost; } - return total_cost.ceil().to_num::(); + return total_cost.round().to_num::(); } + +// Calculates the cost of extra fee for a dedicated node in units usd. +pub fn calculate_extra_fee_cost_units_usd(node_id: u32, seconds_elapsed: u64) -> u64 { + match DedicatedNodesExtraFee::::get(node_id) { + Some(fee_musd_per_month) => { + // Convert fee from mUSD to units USD + let fee_units_usd_per_month = fee_musd_per_month * 10000; + (U64F64::from_num(fee_units_usd_per_month * seconds_elapsed) + / U64F64::from_num(SECS_PER_MONTH)) + .round() + .to_num::() + } + None => 0, + } +} + // cu1 = MAX(cru/2, mru/4) // cu2 = MAX(cru, mru/8) // cu3 = MAX(cru/4, mru/2) @@ -240,10 +273,7 @@ pub fn calculate_discount( certification_type: NodeCertification, ) -> (BalanceOf, types::DiscountLevel) { if amount_due == 0 { - return ( - BalanceOf::::saturated_from(0 as u128), - types::DiscountLevel::None, - ); + return (BalanceOf::::zero(), types::DiscountLevel::None); } // calculate amount due on a monthly basis @@ -277,32 +307,32 @@ pub fn calculate_discount( // convert to balance object let amount_due: BalanceOf = - BalanceOf::::saturated_from(amount_due.ceil().to_num::()); + BalanceOf::::saturated_from(amount_due.round().to_num::()); (amount_due, discount_received) } -pub fn calculate_cost_in_tft_from_musd( - total_cost: u64, +pub fn calculate_cost_in_tft_from_units_usd( + cost_units_usd: u64, ) -> Result { let avg_tft_price = pallet_tft_price::AverageTftPrice::::get(); - // Guaranty tft price will never be lower than min tft price + // Guarantee tft price will never be lower than min tft price let min_tft_price = pallet_tft_price::MinTftPrice::::get(); let mut tft_price = avg_tft_price.max(min_tft_price); - // Guaranty tft price will never be higher than max tft price + // Guarantee tft price will never be higher than max tft price let max_tft_price = pallet_tft_price::MaxTftPrice::::get(); tft_price = tft_price.min(max_tft_price); - // TFT Price is in musd - let tft_price_musd = U64F64::from_num(tft_price); + // TFT Price is in musd so lets convert to units usd + let tft_price_units_usd = tft_price * 10000; - // Cost is expressed in units USD, divide by 10000 to get the price in musd - let total_cost_musd = U64F64::from_num(total_cost) / 10000; + // Calculate cost in TFT + let cost_tft = U64F64::from_num(cost_units_usd) / U64F64::from_num(tft_price_units_usd); - // Now we have the price in musd and cost in musd, make the conversion to the amount of TFT's and multiply by the chain precision (7 decimals) - let total_cost_tft = (total_cost_musd / tft_price_musd) * U64F64::from_num(1e7 as u64); - let total_cost_tft_64: u64 = U64F64::to_num(total_cost_tft); - Ok(total_cost_tft_64) + // Multiply by the chain precision (7 decimals) + Ok((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 7ef371d12..992c93892 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/lib.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/lib.rs @@ -22,7 +22,7 @@ use pallet_tfgrid::types as pallet_tfgrid_types; use pallet_timestamp as timestamp; use sp_core::crypto::KeyTypeId; use sp_runtime::{ - traits::{CheckedSub, Convert, SaturatedConversion}, + traits::{CheckedAdd, CheckedSub, Convert, SaturatedConversion, Zero}, Perbill, }; use sp_std::prelude::*; @@ -213,6 +213,10 @@ pub mod pallet { #[pallet::getter(fn current_migration_stage)] pub(super) type CurrentMigrationStage = StorageValue<_, MigrationStage, OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn dedicated_nodes_extra_fee)] + pub type DedicatedNodesExtraFee = StorageMap<_, Blake2_128Concat, u32, u64, OptionQuery>; + #[pallet::config] pub trait Config: CreateSignedTransaction> @@ -343,6 +347,10 @@ pub mod pallet { amount: BalanceOf, }, BillingFrequencyChanged(u64), + NodeExtraFeeSet { + node_id: u32, + extra_fee: u64, + }, } #[pallet::error] @@ -371,7 +379,7 @@ pub mod pallet { MethodIsDeprecated, NodeHasActiveContracts, NodeHasRentContract, - NodeIsNotDedicated, + FarmIsNotDedicated, NodeNotAvailableToDeploy, CannotUpdateContractInGraceState, NumOverflow, @@ -398,6 +406,7 @@ pub mod pallet { IsNotAnAuthority, WrongAuthority, UnauthorizedToChangeSolutionProviderId, + UnauthorizedToSetExtraFee, } #[pallet::genesis_config] @@ -650,6 +659,17 @@ pub mod pallet { let account_id = ensure_signed(origin)?; Self::_attach_solution_provider_id(account_id, contract_id, solution_provider_id) } + + #[pallet::call_index(20)] + #[pallet::weight(100_000_000 + T::DbWeight::get().writes(1).ref_time() + T::DbWeight::get().reads(1).ref_time())] + pub fn set_dedicated_node_extra_fee( + origin: OriginFor, + node_id: u32, + extra_fee: u64, + ) -> DispatchResultWithPostInfo { + let account_id = ensure_signed(origin)?; + Self::_set_dedicated_node_extra_fee(account_id, node_id, extra_fee) + } } #[pallet::hooks] @@ -737,18 +757,22 @@ impl Pallet { let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; - if farm.dedicated_farm && !ActiveRentContractForNode::::contains_key(node_id) { - return Err(Error::::NodeNotAvailableToDeploy.into()); - } - - // If the user is trying to deploy on a node that has an active rent contract - // only allow the user who created the rent contract to actually deploy a node contract on it + let mut owns_rent_contract = false; if let Some(contract_id) = ActiveRentContractForNode::::get(node_id) { let rent_contract = Contracts::::get(contract_id).ok_or(Error::::ContractNotExists)?; - if rent_contract.twin_id != twin_id { - return Err(Error::::NodeHasRentContract.into()); - } + owns_rent_contract = rent_contract.twin_id == twin_id; + } + + // A node is dedicated (can only be used under a rent contract) + // if it has a dedicated node extra fee or if the farm is dedicated + let node_is_dedicated = + DedicatedNodesExtraFee::::get(node_id).is_some() || farm.dedicated_farm; + + // If the user is not the owner of a supposed rent contract on the node and the node + // is set to be used as dedicated then we don't allow the creation of a node contract. + if !owns_rent_contract && node_is_dedicated { + return Err(Error::::NodeNotAvailableToDeploy.into()); } // If the contract with hash and node id exists and it's in any other state then @@ -1111,7 +1135,7 @@ impl Pallet { log::debug!("nu cost: {:?}", nu_cost); // save total - let total = nu_cost.ceil().to_num::(); + let total = nu_cost.round().to_num::(); log::debug!("total cost: {:?}", total); // update contract billing info @@ -1165,7 +1189,9 @@ impl Pallet { pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; let usable_balance = Self::get_usable_balance(&twin.account_id); let stash_balance = Self::get_stash_balance(twin.id); - let total_balance = usable_balance + stash_balance; + let total_balance = usable_balance + .checked_add(&stash_balance) + .unwrap_or(BalanceOf::::zero()); let now = >::get().saturated_into::() / 1000; @@ -1173,27 +1199,50 @@ impl Pallet { let mut contract_lock = ContractLock::::get(contract.contract_id); let seconds_elapsed = now.checked_sub(contract_lock.lock_updated).unwrap_or(0); - let (amount_due, discount_received) = + // Calculate total amount due + let (regular_amount_due, discount_received) = contract.calculate_contract_cost_tft(total_balance, seconds_elapsed)?; + let extra_amount_due = match &contract.contract_type { + types::ContractData::RentContract(rc) => { + contract.calculate_extra_fee_cost_tft(rc.node_id, seconds_elapsed)? + } + _ => BalanceOf::::zero(), + }; + let amount_due = regular_amount_due + .checked_add(&extra_amount_due) + .unwrap_or(BalanceOf::::zero()); // If there is nothing to be paid and the contract is not in state delete, return // Can be that the users cancels the contract in the same block that it's getting billed // where elapsed seconds would be 0, but we still have to distribute rewards - if amount_due == BalanceOf::::saturated_from(0 as u128) && !contract.is_state_delete() { + if amount_due == BalanceOf::::zero() && !contract.is_state_delete() { log::debug!("amount to be billed is 0, nothing to do"); return Ok(().into()); }; - let total_lock_amount = contract_lock.amount_locked + amount_due; + // Calculate total amount locked + let regular_lock_amount = contract_lock + .amount_locked + .checked_add(®ular_amount_due) + .unwrap_or(BalanceOf::::zero()); + let extra_lock_amount = contract_lock + .extra_amount_locked + .checked_add(&extra_amount_due) + .unwrap_or(BalanceOf::::zero()); + let lock_amount = regular_lock_amount + .checked_add(&extra_lock_amount) + .unwrap_or(BalanceOf::::zero()); + // Handle grace - let contract = Self::handle_grace(&mut contract, usable_balance, total_lock_amount)?; + let contract = Self::handle_grace(&mut contract, usable_balance, lock_amount)?; // Only update contract lock in state (Created, GracePeriod) if !matches!(contract.state, types::ContractState::Deleted(_)) { // increment cycles billed and update the internal lock struct contract_lock.lock_updated = now; contract_lock.cycles += 1; - contract_lock.amount_locked = total_lock_amount; + contract_lock.amount_locked = regular_lock_amount; + contract_lock.extra_amount_locked = extra_lock_amount; } // If still in grace period, no need to continue doing locking and other stuff @@ -1217,8 +1266,7 @@ impl Pallet { // If the contract is in delete state, remove all associated storage if matches!(contract.state, types::ContractState::Deleted(_)) { - Self::remove_contract(contract.contract_id)?; - return Ok(().into()); + return Self::remove_contract(contract.contract_id); } // If contract is node contract, set the amount unbilled back to 0 @@ -1357,7 +1405,9 @@ impl Pallet { pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; if matches!(contract.state, types::ContractState::Created) { let mut locked_balance = Self::get_locked_balance(&twin.account_id); - locked_balance += amount_due; + locked_balance = locked_balance + .checked_add(&amount_due) + .unwrap_or(BalanceOf::::zero()); ::Currency::extend_lock( GRID_LOCK_ID, &twin.account_id, @@ -1367,29 +1417,65 @@ impl Pallet { } let canceled_and_not_zero = - contract.is_state_delete() && contract_lock.amount_locked.saturated_into::() > 0; + contract.is_state_delete() && contract_lock.has_some_amount_locked(); // When the cultivation rewards are ready to be distributed or it's in delete state // Unlock all reserved balance and distribute if contract_lock.cycles >= T::DistributionFrequency::get() || canceled_and_not_zero { // First remove the lock, calculate how much locked balance needs to be unlocked and re-lock the remaining locked balance let locked_balance = Self::get_locked_balance(&twin.account_id); - let new_locked_balance = match locked_balance.checked_sub(&contract_lock.amount_locked) - { - Some(b) => b, - None => BalanceOf::::saturated_from(0 as u128), - }; + let new_locked_balance = + match locked_balance.checked_sub(&contract_lock.total_amount_locked()) { + Some(b) => b, + None => BalanceOf::::zero(), + }; ::Currency::remove_lock(GRID_LOCK_ID, &twin.account_id); - ::Currency::set_lock( - GRID_LOCK_ID, - &twin.account_id, - new_locked_balance, - WithdrawReasons::all(), - ); // Fetch twin balance, if the amount locked in the contract lock exceeds the current unlocked // balance we can only transfer out the remaining balance // https://github.com/threefoldtech/tfchain/issues/479 - let twin_balance = Self::get_usable_balance(&twin.account_id); + let mut twin_balance = Self::get_usable_balance(&twin.account_id); + + if new_locked_balance > ::Currency::minimum_balance() { + // TODO: check if this is needed + ::Currency::set_lock( + GRID_LOCK_ID, + &twin.account_id, + new_locked_balance, + WithdrawReasons::all(), + ); + twin_balance = Self::get_usable_balance(&twin.account_id); + } else { + twin_balance = twin_balance + .checked_sub(&::Currency::minimum_balance()) + .unwrap_or(BalanceOf::::zero()); + }; + + // First, distribute extra cultivation rewards if any + if contract_lock.has_extra_amount_locked() { + log::info!( + "twin balance {:?} contract lock extra amount {:?}", + twin_balance, + contract_lock.extra_amount_locked + ); + + match Self::_distribute_extra_cultivation_rewards( + &contract, + twin_balance.min(contract_lock.extra_amount_locked), + ) { + Ok(_) => {} + Err(err) => { + log::error!( + "error while distributing extra cultivation rewards {:?}", + err + ); + return Err(err); + } + }; + + // Update twin balance after distribution + twin_balance = Self::get_usable_balance(&twin.account_id); + } + log::info!( "twin balance {:?} contract lock amount {:?}", twin_balance, @@ -1399,7 +1485,8 @@ impl Pallet { // Fetch the default pricing policy let pricing_policy = pallet_tfgrid::PricingPolicies::::get(1) .ok_or(Error::::PricingPolicyNotExists)?; - // Distribute cultivation rewards + + // Then, distribute cultivation rewards match Self::_distribute_cultivation_rewards( &contract, &pricing_policy, @@ -1411,9 +1498,11 @@ impl Pallet { return Err(err); } }; - // Reset values + + // Reset contract lock values contract_lock.lock_updated = now; - contract_lock.amount_locked = BalanceOf::::saturated_from(0 as u128); + contract_lock.amount_locked = BalanceOf::::zero(); + contract_lock.extra_amount_locked = BalanceOf::::zero(); contract_lock.cycles = 0; } @@ -1476,6 +1565,58 @@ impl Pallet { Ok(().into()) } + fn _distribute_extra_cultivation_rewards( + contract: &types::Contract, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + log::info!( + "Distributing extra cultivation rewards for contract {:?} with amount {:?}", + contract.contract_id, + amount, + ); + + // If the amount is zero, return + if amount == BalanceOf::::zero() { + return Ok(().into()); + } + + // Fetch source twin = dedicated node user + let src_twin = + pallet_tfgrid::Twins::::get(contract.twin_id).ok_or(Error::::TwinNotExists)?; + + // Fetch destination twin = farmer + let dst_twin = match &contract.contract_type { + types::ContractData::RentContract(rc) => { + let node = + pallet_tfgrid::Nodes::::get(rc.node_id).ok_or(Error::::NodeNotExists)?; + let farm = pallet_tfgrid::Farms::::get(node.farm_id) + .ok_or(Error::::FarmNotExists)?; + pallet_tfgrid::Twins::::get(farm.twin_id).ok_or(Error::::TwinNotExists)? + } + _ => { + return Err(DispatchErrorWithPostInfo::from( + Error::::InvalidContractType, + )); + } + }; + + // Send 100% to the node's owner (farmer) + log::debug!( + "Transfering: {:?} from contract twin {:?} to farmer account {:?}", + &amount, + &src_twin.account_id, + &dst_twin.account_id, + ); + ::Currency::transfer( + &src_twin.account_id, + &dst_twin.account_id, + amount, + ExistenceRequirement::KeepAlive, + )?; + + Ok(().into()) + } + // Following: https://library.threefold.me/info/threefold#/tfgrid/farming/threefold__proof_of_utilization fn _distribute_cultivation_rewards( contract: &types::Contract, @@ -1489,7 +1630,7 @@ impl Pallet { ); // If the amount is zero, return - if amount == BalanceOf::::saturated_from(0 as u128) { + if amount == BalanceOf::::zero() { return Ok(().into()); } @@ -1587,16 +1728,9 @@ impl Pallet { } // Burn 35%, to not have any imbalance in the system, subtract all previously send amounts with the initial - let mut amount_to_burn = + let amount_to_burn = (Perbill::from_percent(50) * amount) - foundation_share - staking_pool_share; - let existential_deposit_requirement = ::Currency::minimum_balance(); - let free_balance = ::Currency::free_balance(&twin.account_id); - if amount_to_burn > free_balance - existential_deposit_requirement { - amount_to_burn = ::Currency::free_balance(&twin.account_id) - - existential_deposit_requirement; - } - let to_burn = T::Currency::withdraw( &twin.account_id, amount_to_burn, @@ -1844,8 +1978,6 @@ impl Pallet { let balance = pallet_balances::pallet::Pallet::::usable_balance(account_id); let b = balance.saturated_into::(); BalanceOf::::saturated_from(b) - .checked_sub(&::Currency::minimum_balance()) - .unwrap_or(BalanceOf::::saturated_from(0 as u128)) } fn get_locked_balance(account_id: &T::AccountId) -> BalanceOf { @@ -1855,7 +1987,7 @@ impl Pallet { let locked_balance = free_balance.checked_sub(&usable_balance); match locked_balance { Some(balance) => balance, - None => BalanceOf::::saturated_from(0 as u128), + None => BalanceOf::::zero(), } } @@ -1863,7 +1995,7 @@ impl Pallet { let account_id = pallet_tfgrid::TwinBoundedAccountID::::get(twin_id); match account_id { Some(account) => Self::get_usable_balance(&account), - None => BalanceOf::::saturated_from(0 as u128), + None => BalanceOf::::zero(), } } @@ -2383,6 +2515,40 @@ impl Pallet { Ok(().into()) } + + pub fn _set_dedicated_node_extra_fee( + account_id: T::AccountId, + node_id: u32, + extra_fee: u64, + ) -> DispatchResultWithPostInfo { + // Nothing to do if fee value is 0 + if extra_fee == 0 { + return Ok(().into()); + } + + // Make sure only the farmer that owns this node can set the extra fee + let twin_id = pallet_tfgrid::TwinIdByAccountID::::get(&account_id) + .ok_or(Error::::TwinNotExists)?; + let node = pallet_tfgrid::Nodes::::get(node_id).ok_or(Error::::NodeNotExists)?; + let farm = pallet_tfgrid::Farms::::get(node.farm_id).ok_or(Error::::FarmNotExists)?; + ensure!( + twin_id == farm.twin_id, + Error::::UnauthorizedToSetExtraFee + ); + + // Make sure there is no active node or rent contract on this node + ensure!( + ActiveRentContractForNode::::get(node_id).is_none() + && ActiveNodeContracts::::get(&node_id).is_empty(), + Error::::NodeHasActiveContracts + ); + + // Set fee in mUSD + DedicatedNodesExtraFee::::insert(node_id, extra_fee); + Self::deposit_event(Event::NodeExtraFeeSet { node_id, extra_fee }); + + Ok(().into()) + } } impl ChangeNode, InterfaceOf, SerialNumberOf> for Pallet { diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/mod.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/mod.rs index a80cc00c7..0c2c7bfa7 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/migrations/mod.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/mod.rs @@ -1,5 +1,7 @@ +pub mod types; pub mod v6; // v7 was a failed migration and was reworked to v8 +pub mod v10; +pub mod v11; pub mod v8; pub mod v9; -pub mod v10; diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/types.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/types.rs new file mode 100644 index 000000000..89125d03e --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/types.rs @@ -0,0 +1,48 @@ +pub mod v10 { + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + #[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Encode, + Decode, + Default, + Debug, + TypeInfo, + MaxEncodedLen, + )] + pub struct ContractLock { + pub amount_locked: BalanceOf, + pub lock_updated: u64, + pub cycles: u16, + } +} + +pub mod v11 { + use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + #[derive( + PartialEq, + Eq, + PartialOrd, + Ord, + Clone, + Encode, + Decode, + Default, + Debug, + TypeInfo, + MaxEncodedLen, + )] + pub struct ContractLock { + pub amount_locked: BalanceOf, + pub extra_amount_locked: BalanceOf, + pub lock_updated: u64, + pub cycles: u16, + } +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs b/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs new file mode 100644 index 000000000..c72b33bd8 --- /dev/null +++ b/substrate-node/pallets/pallet-smart-contract/src/migrations/v11.rs @@ -0,0 +1,121 @@ +use crate::*; +use frame_support::{ + pallet_prelude::ValueQuery, storage_alias, traits::OnRuntimeUpgrade, weights::Weight, + Blake2_128Concat, +}; +use log::{debug, info}; +use sp_std::marker::PhantomData; + +#[cfg(feature = "try-runtime")] +use sp_std::vec::Vec; + +// Storage alias from ContractLock v11 +#[storage_alias] +pub type ContractLock = StorageMap< + Pallet, + Blake2_128Concat, + u64, + super::types::v11::ContractLock>, + ValueQuery, +>; +pub struct ExtendContractLock(PhantomData); +impl OnRuntimeUpgrade for ExtendContractLock { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, &'static str> { + debug!("current pallet version: {:?}", PalletVersion::::get()); + assert!(PalletVersion::::get() >= types::StorageVersion::V10); + + debug!("👥 Smart Contract pallet to V11 passes PRE migrate checks ✅",); + Ok(vec![]) + } + + fn on_runtime_upgrade() -> Weight { + if PalletVersion::::get() >= types::StorageVersion::V10 { + migrate_to_version_11::() + } else { + info!(" >>> Unused Smart Contract pallet V11 migration"); + Weight::zero() + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), &'static str> { + debug!("current pallet version: {:?}", PalletVersion::::get()); + assert!(PalletVersion::::get() >= types::StorageVersion::V11); + + check_contract_lock_v11::(); + + debug!( + "👥 Smart Contract pallet to {:?} passes POST migrate checks ✅", + PalletVersion::::get() + ); + + Ok(()) + } +} + +pub fn migrate_to_version_11() -> frame_support::weights::Weight { + debug!( + " >>> Starting contract pallet migration, pallet version: {:?}", + PalletVersion::::get() + ); + + let mut r = 0; + let mut w = 0; + + // migrate contract locks + ContractLock::::translate::>, _>(|k, fp| { + r += 1; + w += 1; + debug!("Migrating contract lock {:?}", k); + Some(super::types::v11::ContractLock::> { + amount_locked: fp.amount_locked, + // Default to 0 + extra_amount_locked: BalanceOf::::zero(), + lock_updated: fp.lock_updated, + cycles: fp.cycles, + }) + }); + + // Set the new storage version + PalletVersion::::put(types::StorageVersion::V11); + w += 1; + + T::DbWeight::get().reads_writes(r, w) +} + +pub fn check_contract_lock_v11() { + debug!( + "🔎 Smart Contract pallet {:?} checking ContractLock storage map START", + PalletVersion::::get() + ); + + // Check each contract has an associated contract lock + for (contract_id, _) in Contracts::::iter() { + // ContractLock + if !ContractLock::::contains_key(contract_id) { + debug!( + " ⚠️ Contract (id: {}): no contract lock found", + contract_id + ); + } + } + + // Check each contract lock has a valid contract + for (contract_id, contract_lock) in ContractLock::::iter() { + if Contracts::::get(contract_id).is_none() { + debug!( + " ⚠️ ContractLock[contract: {}]: contract not exists", + contract_id + ); + } else { + // Ensure new field is set to zero + assert_eq!(contract_lock.extra_amount_locked, BalanceOf::::zero()); + } + } + + debug!( + "🏁 Smart Contract pallet {:?} checking ContractLock storage map END", + PalletVersion::::get() + ); +} diff --git a/substrate-node/pallets/pallet-smart-contract/src/mock.rs b/substrate-node/pallets/pallet-smart-contract/src/mock.rs index 8d589d391..863615f14 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/mock.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/mock.rs @@ -147,10 +147,12 @@ impl frame_system::Config for TestRuntime { type MaxConsumers = ConstU32<16>; } +pub const EXISTENTIAL_DEPOSIT: u64 = 500; + parameter_types! { pub const MaxLocks: u32 = 50; pub const MaxReserves: u32 = 50; - pub const ExistentialDeposit: u64 = 1; + pub const ExistentialDepositBalance: u64 = EXISTENTIAL_DEPOSIT; } impl pallet_balances::Config for TestRuntime { @@ -162,7 +164,7 @@ impl pallet_balances::Config for TestRuntime { /// The ubiquitous event type. type RuntimeEvent = RuntimeEvent; type DustRemoval = (); - type ExistentialDeposit = ExistentialDeposit; + type ExistentialDeposit = ExistentialDepositBalance; type AccountStore = System; type WeightInfo = pallet_balances::weights::SubstrateWeight; type FreezeIdentifier = (); @@ -489,6 +491,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (alice(), 1000000000000), (bob(), 2500000000), (charlie(), 150000), + (dave(), 1000000000000), ], }; genesis.assimilate_storage(&mut storage).unwrap(); diff --git a/substrate-node/pallets/pallet-smart-contract/src/tests.rs b/substrate-node/pallets/pallet-smart-contract/src/tests.rs index 482703f34..c3e561558 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/tests.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/tests.rs @@ -18,7 +18,7 @@ use sp_core::H256; use sp_runtime::{assert_eq_error_rate, traits::SaturatedConversion, Perbill, Percent}; use sp_std::convert::{TryFrom, TryInto}; use substrate_fixed::types::U64F64; -use tfchain_support::constants::time::SECS_PER_HOUR; +use tfchain_support::constants::time::{SECS_PER_BLOCK, SECS_PER_HOUR}; use tfchain_support::{ resources::Resources, types::{FarmCertification, NodeCertification, PublicIP, IP4}, @@ -861,7 +861,7 @@ fn test_create_node_contract_when_someone_else_has_rent_contract_fails() { 1, None ), - Error::::NodeHasRentContract + Error::::NodeNotAvailableToDeploy ); }) } @@ -1152,10 +1152,9 @@ fn test_node_contract_billing_cycles() { let free_balance = Balances::free_balance(&twin.account_id); let locked_balance = free_balance - usable_balance; - assert_eq_error_rate!( + assert_eq!( locked_balance.saturated_into::(), - amount_due_1 as u128, - 1 + amount_due_1 as u128 ); let (amount_due_2, discount_received) = calculate_tft_cost(contract_id, twin_id, 10); @@ -1177,10 +1176,9 @@ fn test_node_contract_billing_cycles() { let free_balance = Balances::free_balance(&twin.account_id); let locked_balance = free_balance - usable_balance; - assert_eq_error_rate!( + assert_eq!( locked_balance.saturated_into::(), - amount_due_1 as u128 + amount_due_2 as u128 + amount_due_3 as u128, - 3 + amount_due_1 as u128 + amount_due_2 as u128 + amount_due_3 as u128 ); }); } @@ -1257,10 +1255,9 @@ fn test_node_multiple_contract_billing_cycles() { let free_balance = Balances::free_balance(&twin.account_id); let locked_balance = free_balance - usable_balance; - assert_eq_error_rate!( + assert_eq!( locked_balance.saturated_into::(), - amount_due_contract_1 as u128 + amount_due_contract_2 as u128, - 2 + amount_due_contract_1 as u128 + amount_due_contract_2 as u128 ); }); } @@ -1540,10 +1537,9 @@ fn test_node_contract_billing_cycles_cancel_contract_during_cycle_without_balanc // After canceling contract, and not being able to pay for the remainder of the cycle // where the cancel was excecuted, the remaining balance should still be the same let usable_balance_after_canceling = Balances::usable_balance(&twin.account_id); - assert_eq_error_rate!( + assert_eq!( usable_balance_after_canceling, - usable_balance_before_canceling, - 1 + usable_balance_before_canceling ); validate_distribution_rewards(initial_total_issuance, total_amount_billed, false); @@ -1737,11 +1733,6 @@ fn test_node_contract_grace_period_cancels_contract_when_grace_period_ends_works run_to_block(21 + i * 10, Some(&mut pool_state)); } - // pool_state - // .write() - // .should_call_bill_contract(contract_id, Ok(Pays::Yes.into()), 131); - // run_to_block(131, Some(&mut pool_state)); - // The user's total free balance should be distributed let free_balance = Balances::free_balance(&twin.account_id); let total_amount_billed = initial_twin_balance - free_balance; @@ -1782,13 +1773,24 @@ fn test_name_contract_billing() { .should_call_bill_contract(contract_id, Ok(Pays::Yes.into()), 11); run_to_block(11, Some(&mut pool_state)); + // get the contract cost for 1 billing cycle + let contract = SmartContractModule::contracts(contract_id).unwrap(); + let twin_id = 2; + let twin = TfgridModule::twins(twin_id).unwrap(); + let balance = Balances::free_balance(&twin.account_id); + let second_elapsed = BillingFrequency::get() * SECS_PER_BLOCK; + let (contract_cost, _) = contract + .calculate_contract_cost_tft(balance, second_elapsed) + .unwrap(); + // the contractbill event should look like: let contract_bill_event = types::ContractBill { contract_id, timestamp: 1628082066, discount_level: types::DiscountLevel::Gold, - amount_billed: 1848, + amount_billed: contract_cost as u128, }; + let our_events = System::events(); info!("events: {:?}", our_events.clone()); assert_eq!( @@ -1901,7 +1903,7 @@ fn test_rent_contract_billing_cancel_should_bill_reserved_balance() { let usable_balance = Balances::usable_balance(&twin.account_id); let free_balance = Balances::free_balance(&twin.account_id); - assert_eq_error_rate!(usable_balance, free_balance, 2); + assert_eq!(usable_balance, free_balance); }); } @@ -2072,10 +2074,6 @@ fn test_rent_contract_canceled_due_to_out_of_funds_should_cancel_node_contracts_ ); run_to_block(end_grace_block_number, Some(&mut pool_state)); - // let (amount_due_as_u128, discount_received) = calculate_tft_cost(1, 2, 11); - // assert_ne!(amount_due_as_u128, 0); - // check_report_cost(1, 3, amount_due_as_u128, 12, discount_received); - let our_events = System::events(); assert_eq!(our_events.len(), 21); @@ -3389,21 +3387,51 @@ fn test_cu_calculation() { #[test] fn test_lock() { new_test_ext().execute_with(|| { + let usable_balance = Balances::usable_balance(&bob()); + let free_balance = Balances::free_balance(&bob()); + + // should be equal since no activity and no locks + assert_eq!(usable_balance, free_balance); + let id: u64 = 1; + // Try to lock less than EXISTENTIAL_DEPOSIT should fail Balances::set_lock(id.to_be_bytes(), &bob(), 100, WithdrawReasons::all()); + // usable balance should now return free balance - EXISTENTIAL_DEPOSIT cause there was some activity let usable_balance = Balances::usable_balance(&bob()); let free_balance = Balances::free_balance(&bob()); + assert_eq!(usable_balance, free_balance - EXISTENTIAL_DEPOSIT); - let locked_balance = free_balance - usable_balance; - assert_eq!(locked_balance, 100); + // ----- INITIAL ------ // + // Try to lock more than EXISTENTIAL_DEPOSIT should succeed + let to_lock = 100 + EXISTENTIAL_DEPOSIT; - Balances::extend_lock(id.to_be_bytes(), &bob(), 200, WithdrawReasons::all()); + Balances::set_lock(id.to_be_bytes(), &bob(), to_lock, WithdrawReasons::all()); + + // usable balance should now be free_balance - to_lock cause there was some activity let usable_balance = Balances::usable_balance(&bob()); let free_balance = Balances::free_balance(&bob()); + assert_eq!(usable_balance, free_balance - to_lock); - let locked_balance = free_balance - usable_balance; - assert_eq!(locked_balance, 200); + // ----- UPDATE ------ // + // updating a lock should succeed + let to_lock = 500 + EXISTENTIAL_DEPOSIT; + + Balances::set_lock(id.to_be_bytes(), &bob(), to_lock, WithdrawReasons::all()); + + // usable balance should now be free_balance - to_lock cause there was some activity + let usable_balance = Balances::usable_balance(&bob()); + let free_balance = Balances::free_balance(&bob()); + assert_eq!(usable_balance, free_balance - to_lock); + + // ----- UNLOCK ------ // + // Unlock should work + Balances::remove_lock(id.to_be_bytes(), &bob()); + + // usable balance should now be free_balance cause there are no locks + let usable_balance = Balances::usable_balance(&bob()); + let free_balance = Balances::free_balance(&bob()); + assert_eq!(usable_balance, free_balance); }) } @@ -3558,6 +3586,300 @@ fn test_attach_solution_provider_id_not_approved_fails() { }) } +#[test] +fn test_set_dedicated_node_extra_fee_works() { + new_test_ext().execute_with(|| { + run_to_block(1, None); + prepare_farm_and_node(); + let node_id = 1; + + let zero_fee = 0; + assert_ok!(SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + zero_fee + )); + + assert_eq!( + SmartContractModule::dedicated_nodes_extra_fee(node_id), + None + ); + + let extra_fee = 100000; + assert_ok!(SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + )); + + assert_eq!( + SmartContractModule::dedicated_nodes_extra_fee(node_id), + Some(extra_fee) + ); + + let our_events = System::events(); + assert_eq!( + our_events.contains(&record(MockEvent::SmartContractModule( + SmartContractEvent::::NodeExtraFeeSet { node_id, extra_fee } + ))), + true + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_undefined_node_fails() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + let extra_fee = 100000; + assert_noop!( + SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id + 1, + extra_fee + ), + Error::::NodeNotExists + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_unauthorized_fails() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + let extra_fee = 100000; + assert_noop!( + SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(bob()), + node_id, + extra_fee + ), + Error::::UnauthorizedToSetExtraFee + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_with_active_node_contract_fails() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + assert_ok!(SmartContractModule::create_node_contract( + RuntimeOrigin::signed(bob()), + node_id, + generate_deployment_hash(), + get_deployment_data(), + 0, + None + )); + + let extra_fee = 100000; + assert_noop!( + SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + ), + Error::::NodeHasActiveContracts + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_with_active_rent_contract_fails() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + assert_ok!(SmartContractModule::create_rent_contract( + RuntimeOrigin::signed(bob()), + node_id, + None + )); + + let extra_fee = 100000; + assert_noop!( + SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + ), + Error::::NodeHasActiveContracts + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_and_create_node_contract_fails() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + let extra_fee = 100000; + assert_ok!(SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + )); + + assert_noop!( + SmartContractModule::create_node_contract( + RuntimeOrigin::signed(bob()), + node_id, + generate_deployment_hash(), + get_deployment_data(), + 0, + None + ), + Error::::NodeNotAvailableToDeploy + ); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_and_create_rent_contract_works() { + new_test_ext().execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + let extra_fee = 100000; + assert_ok!(SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + )); + + assert_ok!(SmartContractModule::create_rent_contract( + RuntimeOrigin::signed(bob()), + node_id, + None + )); + + assert_ok!(SmartContractModule::create_node_contract( + RuntimeOrigin::signed(bob()), + node_id, + generate_deployment_hash(), + get_deployment_data(), + 0, + None + )); + }) +} + +#[test] +fn test_set_dedicated_node_extra_fee_and_create_rent_contract_billing_works() { + let (mut ext, mut pool_state) = new_test_ext_with_pool_state(0); + ext.execute_with(|| { + prepare_farm_and_node(); + let node_id = 1; + + let start_block = 1; + run_to_block(start_block, None); + + TFTPriceModule::set_prices(RuntimeOrigin::signed(alice()), 50, 101).unwrap(); + + let initial_total_issuance = Balances::total_issuance(); + // Get daves's twin + let twin = TfgridModule::twins(4).unwrap(); + let initial_twin_balance = Balances::free_balance(&twin.account_id); + log::debug!("Twin balance: {}", initial_twin_balance); + + let extra_fee = 100000; + assert_ok!(SmartContractModule::set_dedicated_node_extra_fee( + RuntimeOrigin::signed(alice()), + node_id, + extra_fee + )); + + assert_ok!(SmartContractModule::create_rent_contract( + RuntimeOrigin::signed(dave()), + node_id, + None + )); + + assert_ok!(SmartContractModule::create_node_contract( + RuntimeOrigin::signed(dave()), + node_id, + generate_deployment_hash(), + get_deployment_data(), + 0, + None + )); + + let rent_contract_id = 1; + let rent_contract = SmartContractModule::contracts(rent_contract_id).unwrap(); + + // Ensure contract_id is stored at right billing loop index + let index = SmartContractModule::get_contract_billing_loop_index(rent_contract_id); + assert_eq!( + SmartContractModule::contract_to_bill_at_block(index), + vec![rent_contract_id] + ); + + let now = Timestamp::get().saturated_into::() / 1000; + 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 { + 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)); + + // check why aggregating seconds elapsed is giving different results + let elapsed_time_in_secs = BillingFrequency::get() * SECS_PER_BLOCK; + + // 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) + .unwrap(); + rent_contract_cost_tft += contract_cost_tft; + + // aggregate extra fee cost + extra_fee_cost_tft += rent_contract + .calculate_extra_fee_cost_tft(node_id, elapsed_time_in_secs) + .unwrap(); + } + + let then = Timestamp::get().saturated_into::() / 1000; + let seconds_elapsed = then - now; + log::debug!("seconds elapsed: {}", seconds_elapsed); + + let events = System::events(); + for event in events.iter() { + log::debug!("Event: {:?}", event.event); + } + + let free_balance = Balances::free_balance(&twin.account_id); + let total_amount_billed_tft = initial_twin_balance - free_balance; + log::debug!("total amount billed: {}", total_amount_billed_tft); + log::debug!( + "total amount billed for rent contract: {}", + rent_contract_cost_tft + ); + log::debug!("total amount billed for extra fee: {}", extra_fee_cost_tft); + + // Ensure total amount billed after 24 cycles is equal + // to aggregated rent contract cost + aggregated extra_fee_cost + assert_eq!( + total_amount_billed_tft, + rent_contract_cost_tft + extra_fee_cost_tft + ); + + validate_distribution_rewards(initial_total_issuance, rent_contract_cost_tft, false); + }) +} + #[test] fn test_percent() { let cost: u64 = 1000; @@ -3588,7 +3910,7 @@ macro_rules! test_calculate_discount { let result = cost::calculate_discount::( amount_due, seconds_elapsed, - balance.to_num::(), + balance.round().to_num::(), NodeCertification::Diy, ); @@ -3596,7 +3918,7 @@ macro_rules! test_calculate_discount { result, ( (U64F64::from_num(amount_due) * expected_discount_level.price_multiplier()) - .ceil() + .round() .to_num::(), expected_discount_level ) @@ -3624,7 +3946,7 @@ fn validate_distribution_rewards( total_amount_billed: u64, had_solution_provider: bool, ) { - info!("total locked balance {:?}", total_amount_billed); + info!("total amount billed {:?}", total_amount_billed); let staking_pool_account_balance = Balances::free_balance(&get_staking_pool_account()); info!( @@ -3633,9 +3955,10 @@ fn validate_distribution_rewards( ); // 5% is sent to the staking pool account - assert_eq!( + assert_eq_error_rate!( staking_pool_account_balance, - Perbill::from_percent(5) * total_amount_billed + Perbill::from_percent(5) * total_amount_billed, + 6 ); // 10% is sent to the foundation account @@ -3659,17 +3982,13 @@ fn validate_distribution_rewards( let solution_provider_1_balance = Balances::free_balance(solution_provider.providers[0].who.clone()); info!("solution provider b: {:?}", solution_provider_1_balance); - assert_eq!( - solution_provider_1_balance, - Perbill::from_percent(10) * total_amount_billed - ); + assert_ne!(solution_provider_1_balance, 0); } else { // 50% is sent to the sales account let sales_account_balance = Balances::free_balance(&pricing_policy.certified_sales_account); - assert_eq_error_rate!( + assert_eq!( sales_account_balance, - Perbill::from_percent(50) * total_amount_billed, - 1 + Perbill::from_percent(50) * total_amount_billed ); } @@ -3767,6 +4086,7 @@ pub fn prepare_twins() { create_twin(alice()); create_twin(bob()); create_twin(charlie()); + create_twin(dave()); } pub fn prepare_farm(source: AccountId, dedicated: bool) { diff --git a/substrate-node/pallets/pallet-smart-contract/src/types.rs b/substrate-node/pallets/pallet-smart-contract/src/types.rs index c98919c1d..98b5cb24a 100644 --- a/substrate-node/pallets/pallet-smart-contract/src/types.rs +++ b/substrate-node/pallets/pallet-smart-contract/src/types.rs @@ -1,8 +1,10 @@ use crate::pallet::{MaxDeploymentDataLength, MaxNodeContractPublicIPs}; use crate::Config; +use core::{convert::TryInto, ops::Add}; use frame_support::{pallet_prelude::ConstU32, BoundedVec, RuntimeDebugNoBound}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +use sp_runtime::SaturatedConversion; use sp_std::prelude::*; use substrate_fixed::types::U64F64; use tfchain_support::{resources::Resources, types::PublicIP}; @@ -22,6 +24,7 @@ pub enum StorageVersion { V8, V9, V10, + V11, } impl Default for StorageVersion { @@ -212,10 +215,25 @@ pub struct ContractResources { )] pub struct ContractLock { pub amount_locked: BalanceOf, + pub extra_amount_locked: BalanceOf, pub lock_updated: u64, pub cycles: u16, } +impl + Copy + TryInto> ContractLock { + pub fn total_amount_locked(&self) -> BalanceOf { + self.amount_locked + self.extra_amount_locked + } + + pub fn has_some_amount_locked(&self) -> bool { + self.total_amount_locked().saturated_into::() > 0 + } + + pub fn has_extra_amount_locked(&self) -> bool { + self.extra_amount_locked.saturated_into::() > 0 + } +} + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Encode, Decode, Default, Debug, TypeInfo)] pub struct SolutionProvider { pub solution_provider_id: u64, diff --git a/substrate-node/pallets/pallet-tft-price/src/lib.rs b/substrate-node/pallets/pallet-tft-price/src/lib.rs index 71fe84d4d..1636accc2 100644 --- a/substrate-node/pallets/pallet-tft-price/src/lib.rs +++ b/substrate-node/pallets/pallet-tft-price/src/lib.rs @@ -394,11 +394,7 @@ impl Pallet { } fn queue_transient() -> Box> { - Box::new(RingBufferTransient::< - u32, - BufferRange, - TftPriceHistory, - >::new()) + Box::new(RingBufferTransient::, TftPriceHistory>::new()) } fn calc_avg() -> u32 { diff --git a/substrate-node/runtime/src/lib.rs b/substrate-node/runtime/src/lib.rs index a2d3a767e..94b8949e0 100644 --- a/substrate-node/runtime/src/lib.rs +++ b/substrate-node/runtime/src/lib.rs @@ -766,6 +766,7 @@ pub type Executive = frame_executive::Executive< // `OnRuntimeUpgrade`. type Migrations = ( pallet_smart_contract::migrations::v10::ReworkBillingLoopInsertion, + pallet_smart_contract::migrations::v11::ExtendContractLock, ); // follows Substrate's non destructive way of eliminating otherwise required diff --git a/substrate-node/support/src/constants.rs b/substrate-node/support/src/constants.rs index 709165c04..d8480019d 100644 --- a/substrate-node/support/src/constants.rs +++ b/substrate-node/support/src/constants.rs @@ -7,9 +7,12 @@ pub mod time { /// up by `pallet_aura` to implement `fn slot_duration()`. /// /// Change this to adjust the block time. - pub const MILLISECS_PER_BLOCK: u64 = 6000; + pub const SECS_PER_BLOCK: u64 = 6; + pub const MILLISECS_PER_BLOCK: u64 = SECS_PER_BLOCK * 1000; pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; pub const SECS_PER_HOUR: u64 = 3600; + pub const SECS_PER_DAY: u64 = SECS_PER_HOUR * 24; + pub const SECS_PER_MONTH: u64 = SECS_PER_DAY * 30; pub const EPOCH_DURATION_IN_BLOCKS: super::BlockNumber = 1 * HOURS; // These time units are defined in number of blocks.