Skip to content

Commit

Permalink
feat: grace period for contracts (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanVerstraete authored May 25, 2022
1 parent 0610de2 commit 7e0b37f
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 52 deletions.
10 changes: 10 additions & 0 deletions substrate-node/pallets/pallet-smart-contract/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ Billing will be done in Database Tokens and will be send to the corresponding fa

The main currency of this chain. More information on this is explained here: TODO

## Grace period for contracts

Implements a grace period state `GracePeriod(startBlockNumber)` for all contract types
A grace period is a static amount of time defined by the runtime configuration.


Grace period is triggered if the amount due for a billing cycle is larger than the user's balance.
A grace period is removed on a contract if the next billing cycles notices that the user reloaded the balance on his account.
If this happens, the contract is set back to created state. If a user ignores a graced-out contract, the contract is deleted after the time defined by Grace Period configuration trait.

## Footnote

Sending the workloads encrypted to the chain makes sure that nobody except the destination Node can read the deployment's information as this can contain sensitive data. This way we also don't need to convert all the Zero OS primitive types to a Rust implementation and we can keep it relatively simple.
120 changes: 84 additions & 36 deletions substrate-node/pallets/pallet-smart-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub trait Config:
type Currency: LockableCurrency<Self::AccountId>;
type StakingPoolAccount: Get<Self::AccountId>;
type BillingFrequency: Get<u64>;
type DistributionFrequency: Get<u16>;
type GracePeriod: Get<u64>;
type WeightInfo: WeightInfo;
}

Expand Down Expand Up @@ -67,6 +69,8 @@ decl_event!(
UpdatedUsedResources(types::ContractResources),
NruConsumptionReportReceived(types::NruConsumption),
RentContractCanceled(u64),
ContractGracePeriodStarted(u64, u32, u32, u64),
ContractGracePeriodEnded(u64, u32, u32),
}
);

Expand Down Expand Up @@ -466,7 +470,7 @@ impl<T: Config> Module<T> {
// Update state
Self::_update_contract_state(&mut contract, &types::ContractState::Deleted(cause))?;
// Bill contract
Self::bill_contract(&contract)?;
Self::bill_contract(&mut contract)?;
// Remove all associated storage
Self::remove_contract(contract.contract_id);

Expand Down Expand Up @@ -645,8 +649,7 @@ impl<T: Config> Module<T> {

// Bills a contract (NodeContract or NameContract)
// Calculates how much TFT is due by the user and distributes the rewards
// Will also cancel the contract if there is not enough funds on the users wallet
fn bill_contract(contract: &types::Contract) -> DispatchResult {
fn bill_contract(contract: &mut types::Contract) -> DispatchResult {
if !pallet_tfgrid::Twins::<T>::contains_key(contract.twin_id) {
return Err(DispatchError::from(Error::<T>::TwinNotExists));
}
Expand All @@ -655,51 +658,118 @@ impl<T: Config> Module<T> {

let mut seconds_elapsed = T::BillingFrequency::get() * 6;
// Calculate amount of seconds elapsed based on the contract lock struct
let mut contract_lock = ContractLock::<T>::get(contract.contract_id);

let now = <timestamp::Module<T>>::get().saturated_into::<u64>() / 1000;

// this will set the seconds elapsed to the default billing cycle duration in seconds
// if there is no contract lock object yet. A contract lock object will be created later in this function
// https://github.com/threefoldtech/tfchain/issues/261
let contract_lock = ContractLock::<T>::get(contract.contract_id);
if contract_lock.lock_updated != 0 {
seconds_elapsed = now.checked_sub(contract_lock.lock_updated).unwrap_or(0);
}

let (mut amount_due, discount_received) =
let (amount_due, discount_received) =
Self::calculate_contract_cost_tft(contract, usable_balance, seconds_elapsed)?;

// If there is nothing to be paid, return
if amount_due == BalanceOf::<T>::saturated_from(0 as u128) {
return Ok(());
};

// Handle grace
let contract = Self::handle_grace(contract, usable_balance, amount_due)?;

// Handle contract lock operations
Self::handle_lock(contract, amount_due);

// Always emit a contract billed event
let contract_bill = types::ContractBill {
contract_id: contract.contract_id,
timestamp: <timestamp::Module<T>>::get().saturated_into::<u64>() / 1000,
discount_level: discount_received.clone(),
amount_billed: amount_due.saturated_into::<u128>(),
};
Self::deposit_event(RawEvent::ContractBilled(contract_bill));

// set the amount unbilled back to 0
let mut contract_billing_info = ContractBillingInformationByID::get(contract.contract_id);
contract_billing_info.amount_unbilled = 0;
ContractBillingInformationByID::insert(contract.contract_id, &contract_billing_info);

// If the contract is in delete state, remove all associated storage
if matches!(contract.state, types::ContractState::Deleted(_)) {
Self::remove_contract(
contract.contract_id,
);
}

// if the total amount due exceeds the twin's balance we must decomission the contract
let mut decomission = false;
if amount_due >= usable_balance {
decomission = true;
amount_due = usable_balance;
Ok(())
}

fn handle_grace(contract: &mut types::Contract, usable_balance: BalanceOf<T>, amount_due: BalanceOf<T>) -> Result<&mut types::Contract, DispatchError> {
let current_block = <frame_system::Module<T>>::block_number().saturated_into::<u64>();
let node_id = contract.get_node_id();

match contract.state {
types::ContractState::GracePeriod(grace_start) => {
// if the usable balance is recharged, we can move the contract to created state again
if usable_balance > amount_due {
Self::_update_contract_state(contract, &types::ContractState::Created)?;
Self::deposit_event(RawEvent::ContractGracePeriodEnded(contract.contract_id, node_id, contract.twin_id))
} else {
let diff = current_block - grace_start;
// If the contract grace period ran out, we can decomission the contract
if diff >= T::GracePeriod::get() {
Self::_update_contract_state(contract, &types::ContractState::Deleted(types::Cause::OutOfFunds))?;
}
}
}
_ => {
// if the user ran out of funds, move the contract to be in a grace period
// dont lock the tokens because there is nothing to lock
// we can still update the internal contract lock object to figure out later how much was due
// whilst in grace period
if amount_due >= usable_balance {
Self::_update_contract_state(contract, &types::ContractState::GracePeriod(current_block))?;
// We can't lock the amount due on the contract's lock because the user ran out of funds
Self::deposit_event(RawEvent::ContractGracePeriodStarted(contract.contract_id, node_id, contract.twin_id, current_block.saturated_into()));
}
}
}

Ok(contract)
}

fn handle_lock(contract: &mut types::Contract, amount_due: BalanceOf<T>) {
if matches!(contract.state, types::ContractState::GracePeriod(_)) {
return
}

let twin = pallet_tfgrid::Twins::<T>::get(contract.twin_id);
let now = <timestamp::Module<T>>::get().saturated_into::<u64>() / 1000;
let mut contract_lock = ContractLock::<T>::get(contract.contract_id);
let new_amount_locked = contract_lock.amount_locked + amount_due;

// Only lock an amount from the user's balance if the contract is in create state
// Update lock for contract and ContractLock in storage
<T as Config>::Currency::extend_lock(
contract.contract_id.to_be_bytes(),
&twin.account_id,
new_amount_locked.into(),
WithdrawReasons::RESERVE,
);
// increment cycles billed

// increment cycles billed and update the internal lock struct
contract_lock.lock_updated = now;
contract_lock.cycles += 1;
contract_lock.amount_locked = new_amount_locked;
ContractLock::<T>::insert(contract.contract_id, &contract_lock);

let is_canceled = matches!(contract.state, types::ContractState::Deleted(_));
// When the cultivation rewards are ready to be distributed or we have to decomission the contract (due to out of funds) or it's canceled by the user
let canceled_and_not_zero = is_canceled && contract_lock.amount_locked.saturated_into::<u64>() > 0;
// 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 == 24 || decomission || is_canceled {
if contract_lock.cycles == T::DistributionFrequency::get() || canceled_and_not_zero {
// Remove lock
<T as Config>::Currency::remove_lock(
contract.contract_id.to_be_bytes(),
Expand All @@ -722,28 +792,6 @@ impl<T: Config> Module<T> {
contract_lock.cycles = 0;
ContractLock::<T>::insert(contract.contract_id, &contract_lock);
}

let contract_bill = types::ContractBill {
contract_id: contract.contract_id,
timestamp: <timestamp::Module<T>>::get().saturated_into::<u64>() / 1000,
discount_level: discount_received.clone(),
amount_billed: amount_due.saturated_into::<u128>(),
};
Self::deposit_event(RawEvent::ContractBilled(contract_bill));

// set the amount unbilled back to 0
let mut contract_billing_info = ContractBillingInformationByID::get(contract.contract_id);
contract_billing_info.amount_unbilled = 0;
ContractBillingInformationByID::insert(contract.contract_id, &contract_billing_info);

// If total balance exceeds the twin's balance, we can decomission contract
if decomission {
Self::remove_contract(
contract.contract_id,
);
}

Ok(())
}

fn calculate_contract_cost_tft(
Expand Down
4 changes: 4 additions & 0 deletions substrate-node/pallets/pallet-smart-contract/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ impl pallet_timestamp::Config for TestRuntime {

parameter_types! {
pub const BillingFrequency: u64 = 10;
pub const GracePeriod: u64 = 100;
pub const DistributionFrequency: u16 = 24;
}

use weights;
Expand All @@ -113,6 +115,8 @@ impl Config for TestRuntime {
type Currency = Balances;
type StakingPoolAccount = StakingPoolAccount;
type BillingFrequency = BillingFrequency;
type DistributionFrequency = DistributionFrequency;
type GracePeriod = GracePeriod;
type WeightInfo = weights::SubstrateWeight<TestRuntime>;
}

Expand Down
Loading

0 comments on commit 7e0b37f

Please sign in to comment.