git.delta.rocks / unique-network / refs/commits / 8ee00409a037

difftreelog

source

pallets/app-promotion/src/lib.rs16.8 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # App promotion18//!19//! The app promotion pallet is designed to ... .20//!21//! ## Interface22//!23//! ### Dispatchable Functions24//!25//! * `start_inflation` - This method sets the inflation start date. Can be only called once.26//! Inflation start block can be backdated and will catch up. The method will create Treasury27//!	account if it does not exist and perform the first inflation deposit.2829// #![recursion_limit = "1024"]30#![cfg_attr(not(feature = "std"), no_std)]3132#[cfg(feature = "runtime-benchmarks")]33mod benchmarking;34#[cfg(test)]35mod tests;36pub mod types;37pub mod weights;3839use sp_std::{vec::Vec, iter::Sum, borrow::ToOwned};40use sp_core::H160;41use codec::EncodeLike;42use pallet_balances::BalanceLock;43pub use types::*;4445// use up_common::constants::{DAYS, UNIQUE};46use up_data_structs::CollectionId;4748use frame_support::{49	dispatch::{DispatchResult},50	traits::{51		Currency, Get, LockableCurrency, WithdrawReasons, tokens::Balance, ExistenceRequirement,52	},53	ensure,54};5556use weights::WeightInfo;5758pub use pallet::*;59use pallet_evm::account::CrossAccountId;60use sp_runtime::{61	Perbill,62	traits::{BlockNumberProvider, CheckedAdd, CheckedSub, AccountIdConversion},63	ArithmeticError,64};6566type BalanceOf<T> =67	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;6869// const SECONDS_TO_BLOCK: u32 = 6;70// const DAY: u32 = 60 * 60 * 24 / SECONDS_TO_BLOCK;71// const WEEK: u32 = 7 * DAY;72// const TWO_WEEK: u32 = 2 * WEEK;73// const YEAR: u32 = DAY * 365;7475pub const LOCK_IDENTIFIER: [u8; 8] = *b"appstake";7677#[frame_support::pallet]78pub mod pallet {79	use super::*;80	use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId};81	use frame_system::pallet_prelude::*;8283	#[pallet::config]84	pub trait Config: frame_system::Config + pallet_evm::account::Config {85		type Currency: ExtendedLockableCurrency<Self::AccountId>;8687		type CollectionHandler: CollectionHandler<88			AccountId = Self::AccountId,89			CollectionId = CollectionId,90		>;9192		type ContractHandler: ContractHandler<AccountId = Self::CrossAccountId, ContractId = H160>;9394		type TreasuryAccountId: Get<Self::AccountId>;9596		/// The app's pallet id, used for deriving its sovereign account ID.97		#[pallet::constant]98		type PalletId: Get<PalletId>;99100		/// In relay blocks.101		#[pallet::constant]102		type RecalculationInterval: Get<Self::BlockNumber>;103		/// In relay blocks.104		#[pallet::constant]105		type PendingInterval: Get<Self::BlockNumber>;106107		/// In chain blocks.108		#[pallet::constant]109		type Day: Get<Self::BlockNumber>; // useless110111		#[pallet::constant]112		type Nominal: Get<BalanceOf<Self>>;113114		#[pallet::constant]115		type IntervalIncome: Get<Perbill>;116117		/// Weight information for extrinsics in this pallet.118		type WeightInfo: WeightInfo;119120		// The relay block number provider121		type RelayBlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;122123		/// Events compatible with [`frame_system::Config::Event`].124		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;125	}126127	#[pallet::pallet]128	#[pallet::generate_store(pub(super) trait Store)]129	pub struct Pallet<T>(_);130131	#[pallet::event]132	#[pallet::generate_deposit(fn deposit_event)]133	pub enum Event<T: Config> {134		StakingRecalculation(135			/// Base on which interest is calculated136			BalanceOf<T>,137			/// Amount of accrued interest138			BalanceOf<T>,139		),140	}141142	#[pallet::error]143	pub enum Error<T> {144		/// Error due to action requiring admin to be set145		AdminNotSet,146		/// No permission to perform an action147		NoPermission,148		/// Insufficient funds to perform an action149		NotSufficientFounds,150		/// An error related to the fact that an invalid argument was passed to perform an action151		InvalidArgument,152	}153154	#[pallet::storage]155	pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;156157	#[pallet::storage]158	pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;159160	/// Amount of tokens staked by account in the blocknumber.161	#[pallet::storage]162	pub type Staked<T: Config> = StorageNMap<163		Key = (164			Key<Blake2_128Concat, T::AccountId>,165			Key<Twox64Concat, T::BlockNumber>,166		),167		Value = BalanceOf<T>,168		QueryKind = ValueQuery,169	>;170171	/// Amount of tokens pending unstake per user per block.172	#[pallet::storage]173	pub type PendingUnstake<T: Config> = StorageNMap<174		Key = (175			Key<Blake2_128Concat, T::AccountId>,176			Key<Twox64Concat, T::BlockNumber>,177		),178		Value = BalanceOf<T>,179		QueryKind = ValueQuery,180	>;181182	/// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.183	#[pallet::storage]184	pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;185186	/// Next target block when interest is recalculated187	#[pallet::storage]188	#[pallet::getter(fn get_interest_block)]189	pub type NextInterestBlock<T: Config> =190		StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;191192	#[pallet::hooks]193	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {194		fn on_initialize(current_block: T::BlockNumber) -> Weight195		where196			<T as frame_system::Config>::BlockNumber: From<u32>,197		{198			let mut consumed_weight = 0;199			let mut add_weight = |reads, writes, weight| {200				consumed_weight += T::DbWeight::get().reads_writes(reads, writes);201				consumed_weight += weight;202			};203204			PendingUnstake::<T>::iter()205				.filter_map(|((staker, block), amount)| {206					if block <= current_block {207						Some((staker, block, amount))208					} else {209						None210					}211				})212				.for_each(|(staker, block, amount)| {213					Self::unlock_balance_unchecked(&staker, amount); // TO-DO : Replace with a method that will check that the unstack is less than it was blocked, otherwise take the delta from the treasuries214					<PendingUnstake<T>>::remove((staker, block));215				});216217			let next_interest_block = Self::get_interest_block();218			let current_relay_block = T::RelayBlockNumberProvider::current_block_number();219			if next_interest_block != 0.into() && current_relay_block >= next_interest_block {220				let mut acc = <BalanceOf<T>>::default();221				let mut base_acc = <BalanceOf<T>>::default();222223				NextInterestBlock::<T>::set(224					NextInterestBlock::<T>::get() + T::RecalculationInterval::get(),225				);226				add_weight(0, 1, 0);227228				Staked::<T>::iter()229					.filter(|((_, block), _)| {230						*block + T::RecalculationInterval::get() <= current_relay_block231					})232					.for_each(|((staker, block), amount)| {233						Self::recalculate_stake(&staker, block, amount, &mut acc);234						add_weight(0, 0, T::WeightInfo::recalculate_stake());235						base_acc += amount;236					});237				<TotalStaked<T>>::get()238					.checked_add(&acc)239					.map(|res| <TotalStaked<T>>::set(res));240241				Self::deposit_event(Event::StakingRecalculation(base_acc, acc));242				add_weight(0, 1, 0);243			} else {244				add_weight(1, 0, 0)245			};246			consumed_weight247		}248	}249250	#[pallet::call]251	impl<T: Config> Pallet<T> {252		#[pallet::weight(T::WeightInfo::set_admin_address())]253		pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {254			ensure_root(origin)?;255			<Admin<T>>::set(Some(admin.as_sub().to_owned()));256257			Ok(())258		}259260		#[pallet::weight(T::WeightInfo::start_app_promotion())]261		pub fn start_app_promotion(262			origin: OriginFor<T>,263			promotion_start_relay_block: Option<T::BlockNumber>,264		) -> DispatchResult265		where266			<T as frame_system::Config>::BlockNumber: From<u32>,267		{268			ensure_root(origin)?;269270			// Start app-promotion mechanics if it has not been yet initialized271			if <StartBlock<T>>::get() == 0u32.into() {272				let start_block = promotion_start_relay_block273					.unwrap_or(T::RelayBlockNumberProvider::current_block_number());274275				// Set promotion global start block276				<StartBlock<T>>::set(start_block);277278				<NextInterestBlock<T>>::set(start_block + T::RecalculationInterval::get());279			}280281			Ok(())282		}283284		#[pallet::weight(0)]285		pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult286		where287			<T as frame_system::Config>::BlockNumber: From<u32>,288		{289			ensure_root(origin)?;290291			if <StartBlock<T>>::get() != 0u32.into() {292				<StartBlock<T>>::set(T::BlockNumber::default());293				<NextInterestBlock<T>>::set(T::BlockNumber::default());294			}295296			Ok(())297		}298299		#[pallet::weight(T::WeightInfo::stake())]300		pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {301			let staker_id = ensure_signed(staker)?;302303			ensure!(amount >= T::Nominal::get(), ArithmeticError::Underflow);304305			let balance =306				<<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);307308			ensure!(balance >= amount, ArithmeticError::Underflow);309310			<<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(311				&staker_id,312				amount,313				WithdrawReasons::all(),314				balance - amount,315			)?;316317			Self::add_lock_balance(&staker_id, amount)?;318319			let block_number = T::RelayBlockNumberProvider::current_block_number();320321			<Staked<T>>::insert(322				(&staker_id, block_number),323				<Staked<T>>::get((&staker_id, block_number))324					.checked_add(&amount)325					.ok_or(ArithmeticError::Overflow)?,326			);327328			<TotalStaked<T>>::set(329				<TotalStaked<T>>::get()330					.checked_add(&amount)331					.ok_or(ArithmeticError::Overflow)?,332			);333334			Ok(())335		}336337		#[pallet::weight(T::WeightInfo::unstake())]338		pub fn unstake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {339			let staker_id = ensure_signed(staker)?;340341			let mut stakes = Staked::<T>::iter_prefix((&staker_id,)).collect::<Vec<_>>();342343			let total_staked = stakes344				.iter()345				.fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + *amount);346347			ensure!(total_staked >= amount, ArithmeticError::Underflow);348349			<TotalStaked<T>>::set(350				<TotalStaked<T>>::get()351					.checked_sub(&amount)352					.ok_or(ArithmeticError::Underflow)?,353			);354355			let block =356				T::RelayBlockNumberProvider::current_block_number() + T::PendingInterval::get();357			<PendingUnstake<T>>::insert(358				(&staker_id, block),359				<PendingUnstake<T>>::get((&staker_id, block))360					.checked_add(&amount)361					.ok_or(ArithmeticError::Overflow)?,362			);363364			stakes.sort_by_key(|(block, _)| *block);365366			let mut acc_amount = amount;367			let new_state = stakes368				.into_iter()369				.map_while(|(block, balance_per_block)| {370					if acc_amount == <BalanceOf<T>>::default() {371						return None;372					}373					if acc_amount <= balance_per_block {374						let res = (block, balance_per_block - acc_amount, acc_amount);375						acc_amount = <BalanceOf<T>>::default();376						return Some(res);377					} else {378						acc_amount -= balance_per_block;379						return Some((block, <BalanceOf<T>>::default(), acc_amount));380					}381				})382				.collect::<Vec<_>>();383384			new_state385				.into_iter()386				.for_each(|(block, to_staked, _to_pending)| {387					if to_staked == <BalanceOf<T>>::default() {388						<Staked<T>>::remove((&staker_id, block));389					} else {390						<Staked<T>>::insert((&staker_id, block), to_staked);391					}392				});393394			Ok(())395		}396397		#[pallet::weight(0)]398		pub fn sponsor_collection(399			admin: OriginFor<T>,400			collection_id: CollectionId,401		) -> DispatchResult {402			let admin_id = ensure_signed(admin)?;403			ensure!(404				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,405				Error::<T>::NoPermission406			);407408			T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)409		}410		#[pallet::weight(0)]411		pub fn stop_sponsorign_collection(412			admin: OriginFor<T>,413			collection_id: CollectionId,414		) -> DispatchResult {415			let admin_id = ensure_signed(admin)?;416417			ensure!(418				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,419				Error::<T>::NoPermission420			);421422			ensure!(423				T::CollectionHandler::get_sponsor(collection_id)?424					.ok_or(<Error<T>>::InvalidArgument)?425					== Self::account_id(),426				<Error<T>>::NoPermission427			);428			T::CollectionHandler::remove_collection_sponsor(collection_id)429		}430431		#[pallet::weight(0)]432		pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {433			let admin_id = ensure_signed(admin)?;434435			ensure!(436				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,437				Error::<T>::NoPermission438			);439440			T::ContractHandler::set_sponsor(441				T::CrossAccountId::from_sub(Self::account_id()),442				contract_id,443			)444		}445446		#[pallet::weight(0)]447		pub fn stop_sponsorign_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {448			let admin_id = ensure_signed(admin)?;449450			ensure!(451				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,452				Error::<T>::NoPermission453			);454455			ensure!(456				T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?457					== T::CrossAccountId::from_sub(Self::account_id()),458				<Error<T>>::NoPermission459			);460			T::ContractHandler::remove_contract_sponsor(contract_id)461		}462	}463}464465impl<T: Config> Pallet<T> {466	pub fn account_id() -> T::AccountId {467		T::PalletId::get().into_account_truncating()468	}469470	fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {471		let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();472		locked_balance -= amount;473		Self::set_lock_unchecked(staker, locked_balance);474	}475476	fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {477		Self::get_locked_balance(staker)478			.map_or(<BalanceOf<T>>::default(), |l| l.amount)479			.checked_add(&amount)480			.map(|new_lock| Self::set_lock_unchecked(staker, new_lock))481			.ok_or(ArithmeticError::Overflow.into())482	}483484	fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {485		<T::Currency as LockableCurrency<T::AccountId>>::set_lock(486			LOCK_IDENTIFIER,487			staker,488			amount,489			WithdrawReasons::all(),490		)491	}492493	pub fn get_locked_balance(494		staker: impl EncodeLike<T::AccountId>,495	) -> Option<BalanceLock<BalanceOf<T>>> {496		<T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)497			.into_iter()498			.find(|l| l.id == LOCK_IDENTIFIER)499	}500501	pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {502		let staked = Staked::<T>::iter_prefix((staker,))503			.into_iter()504			.fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + amount);505		if staked != <BalanceOf<T>>::default() {506			Some(staked)507		} else {508			None509		}510	}511512	pub fn total_staked_by_id_per_block(513		staker: impl EncodeLike<T::AccountId>,514	) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {515		let mut staked = Staked::<T>::iter_prefix((staker,))516			.into_iter()517			.map(|(block, amount)| (block, amount))518			.collect::<Vec<_>>();519		staked.sort_by_key(|(block, _)| *block);520		if !staked.is_empty() {521			Some(staked)522		} else {523			None524		}525	}526527	pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {528		staker.map_or(Some(<TotalStaked<T>>::get()), |s| {529			Self::total_staked_by_id(s.as_sub())530		})531		// Self::total_staked_by_id(staker.as_sub())532	}533534	pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {535		Self::get_locked_balance(staker.as_sub())536			.map(|l| l.amount)537			.unwrap_or_default()538	}539540	pub fn cross_id_total_staked_per_block(541		staker: T::CrossAccountId,542	) -> Vec<(T::BlockNumber, BalanceOf<T>)> {543		Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()544	}545546	fn recalculate_stake(547		staker: &T::AccountId,548		block: T::BlockNumber,549		base: BalanceOf<T>,550		income_acc: &mut BalanceOf<T>,551	) {552		let income = Self::calculate_income(base);553		base.checked_add(&income).map(|res| {554			<Staked<T>>::insert((staker, block), res);555			*income_acc += income;556			<T::Currency as Currency<T::AccountId>>::transfer(557				&T::TreasuryAccountId::get(),558				staker,559				income,560				ExistenceRequirement::KeepAlive,561			)562			.and_then(|_| Self::add_lock_balance(staker, income));563		});564	}565566	fn calculate_income<I>(base: I) -> I567	where568		I: EncodeLike<BalanceOf<T>> + Balance,569	{570		T::IntervalIncome::get() * base571	}572}573574impl<T: Config> Pallet<T>575where576	<<T as pallet::Config>::Currency as Currency<T::AccountId>>::Balance: Sum,577{578	pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {579		staker.map_or(PendingUnstake::<T>::iter_values().sum(), |s| {580			PendingUnstake::<T>::iter_prefix_values((s.as_sub(),)).sum()581		})582	}583584	pub fn cross_id_pending_unstake_per_block(585		staker: T::CrossAccountId,586	) -> Vec<(T::BlockNumber, BalanceOf<T>)> {587		let mut unsorted_res = PendingUnstake::<T>::iter_prefix((staker.as_sub(),))588			.into_iter()589			.collect::<Vec<_>>();590		unsorted_res.sort_by_key(|(block, _)| *block);591		unsorted_res592	}593}