git.delta.rocks / unique-network / refs/commits / 081dbb6ae4fb

difftreelog

source

pallets/app-promotion/src/lib.rs20.2 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, Zero},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			/// An recalculated staker136			T::AccountId,137			/// Base on which interest is calculated138			BalanceOf<T>,139			/// Amount of accrued interest140			BalanceOf<T>,141		),142	}143144	#[pallet::error]145	pub enum Error<T> {146		/// Error due to action requiring admin to be set147		AdminNotSet,148		/// No permission to perform an action149		NoPermission,150		/// Insufficient funds to perform an action151		NotSufficientFounds,152		/// An error related to the fact that an invalid argument was passed to perform an action153		InvalidArgument,154	}155156	#[pallet::storage]157	pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;158159	#[pallet::storage]160	pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;161162	/// Amount of tokens staked by account in the blocknumber.163	#[pallet::storage]164	pub type Staked<T: Config> = StorageNMap<165		Key = (166			Key<Blake2_128Concat, T::AccountId>,167			Key<Twox64Concat, T::BlockNumber>,168		),169		Value = (BalanceOf<T>, T::BlockNumber),170		QueryKind = ValueQuery,171	>;172	/// Amount of stakes for an Account173	#[pallet::storage]174	pub type StakesPerAccount<T: Config> =175		StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;176177	/// Amount of tokens pending unstake per user per block.178	#[pallet::storage]179	pub type PendingUnstake<T: Config> = StorageNMap<180		Key = (181			Key<Blake2_128Concat, T::AccountId>,182			Key<Twox64Concat, T::BlockNumber>,183		),184		Value = BalanceOf<T>,185		QueryKind = ValueQuery,186	>;187188	/// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.189	#[pallet::storage]190	pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;191192	/// Next target block when interest is recalculated193	#[pallet::storage]194	#[pallet::getter(fn get_interest_block)]195	pub type NextInterestBlock<T: Config> =196		StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;197198	/// Stores hash a record for which the last revenue recalculation was performed.199	/// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.200	#[pallet::storage]201	#[pallet::getter(fn get_next_calculated_record)]202	pub type NextCalculatedRecord<T: Config> =203		StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;204205	#[pallet::hooks]206	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {207		fn on_initialize(current_block: T::BlockNumber) -> Weight208		where209			<T as frame_system::Config>::BlockNumber: From<u32>,210		{211			let mut consumed_weight = 0;212			// let mut add_weight = |reads, writes, weight| {213			// 	consumed_weight += T::DbWeight::get().reads_writes(reads, writes);214			// 	consumed_weight += weight;215			// };216217			let current_relay_block = T::RelayBlockNumberProvider::current_block_number();218			PendingUnstake::<T>::iter()219				.filter_map(|((staker, block), amount)| {220					if block <= current_relay_block {221						Some((staker, block, amount))222					} else {223						None224					}225				})226				.for_each(|(staker, block, amount)| {227					Self::unlock_balance_unchecked(&staker, amount);228					<PendingUnstake<T>>::remove((staker, block));229				});230231			// let next_interest_block = Self::get_interest_block();232			// let current_relay_block = T::RelayBlockNumberProvider::current_block_number();233			// if next_interest_block != 0.into() && current_relay_block >= next_interest_block {234			// 	let mut acc = <BalanceOf<T>>::default();235			// 	let mut base_acc = <BalanceOf<T>>::default();236237			// 	NextInterestBlock::<T>::set(238			// 		NextInterestBlock::<T>::get() + T::RecalculationInterval::get(),239			// 	);240			// 	add_weight(0, 1, 0);241242			// 	Staked::<T>::iter()243			// 		.filter(|((_, block), _)| {244			// 			*block + T::RecalculationInterval::get() <= current_relay_block245			// 		})246			// 		.for_each(|((staker, block), amount)| {247			// 			Self::recalculate_stake(&staker, block, amount, &mut acc);248			// 			add_weight(0, 0, T::WeightInfo::recalculate_stake());249			// 			base_acc += amount;250			// 		});251			// 	<TotalStaked<T>>::get()252			// 		.checked_add(&acc)253			// 		.map(|res| <TotalStaked<T>>::set(res));254255			// 	Self::deposit_event(Event::StakingRecalculation(base_acc, acc));256			// 	add_weight(0, 1, 0);257			// } else {258			// 	add_weight(1, 0, 0)259			// };260			consumed_weight261		}262	}263264	#[pallet::call]265	impl<T: Config> Pallet<T>266	where267		T::BlockNumber: From<u32> + Into<u32>,268		<<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum + From<u128>,269	{270		#[pallet::weight(T::WeightInfo::set_admin_address())]271		pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {272			ensure_root(origin)?;273			<Admin<T>>::set(Some(admin.as_sub().to_owned()));274275			Ok(())276		}277278		#[pallet::weight(T::WeightInfo::start_app_promotion())]279		pub fn start_app_promotion(280			origin: OriginFor<T>,281			promotion_start_relay_block: Option<T::BlockNumber>,282		) -> DispatchResult283		where284			<T as frame_system::Config>::BlockNumber: From<u32>,285		{286			ensure_root(origin)?;287288			// Start app-promotion mechanics if it has not been yet initialized289			if <StartBlock<T>>::get() == 0u32.into() {290				let start_block = promotion_start_relay_block291					.unwrap_or(T::RelayBlockNumberProvider::current_block_number());292293				// Set promotion global start block294				<StartBlock<T>>::set(start_block);295296				<NextInterestBlock<T>>::set(start_block + T::RecalculationInterval::get());297			}298299			Ok(())300		}301302		#[pallet::weight(T::WeightInfo::stop_app_promotion())]303		pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult304		where305			<T as frame_system::Config>::BlockNumber: From<u32>,306		{307			ensure_root(origin)?;308309			if <StartBlock<T>>::get() != 0u32.into() {310				<StartBlock<T>>::set(T::BlockNumber::default());311				<NextInterestBlock<T>>::set(T::BlockNumber::default());312			}313314			Ok(())315		}316317		#[pallet::weight(T::WeightInfo::stake())]318		pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {319			let staker_id = ensure_signed(staker)?;320321			ensure!(322				StakesPerAccount::<T>::get(&staker_id) < 10,323				Error::<T>::NoPermission324			);325326			ensure!(327				amount >= Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get(),328				ArithmeticError::Underflow329			);330331			let count = Staked::<T>::iter_prefix((staker_id.clone(),)).count();332333			let balance =334				<<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);335336			ensure!(balance >= amount, ArithmeticError::Underflow);337338			<<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(339				&staker_id,340				amount,341				WithdrawReasons::all(),342				balance - amount,343			)?;344345			Self::add_lock_balance(&staker_id, amount)?;346347			let block_number = T::RelayBlockNumberProvider::current_block_number();348			let recalc_block = (block_number / T::RecalculationInterval::get() + 2u32.into())349				* T::RecalculationInterval::get();350351			<Staked<T>>::insert((&staker_id, block_number), {352				let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));353				balance_and_recalc_block.0 = balance_and_recalc_block354					.0355					.checked_add(&amount)356					.ok_or(ArithmeticError::Overflow)?;357				balance_and_recalc_block.1 = recalc_block;358				balance_and_recalc_block359			});360361			<TotalStaked<T>>::set(362				<TotalStaked<T>>::get()363					.checked_add(&amount)364					.ok_or(ArithmeticError::Overflow)?,365			);366367			StakesPerAccount::<T>::mutate(&staker_id, |stakes| *stakes += 1);368			Ok(())369		}370371		#[pallet::weight(T::WeightInfo::unstake())]372		pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {373			let staker_id = ensure_signed(staker)?;374375			let mut total_stakes = 0u64;376377			let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))378				.map(|(_, (amount, _))| {379					total_stakes += 1;380					amount381				})382				.sum();383			384			if total_staked.is_zero() {385				return Ok(None.into());386			}387			let block =388				T::RelayBlockNumberProvider::current_block_number() + T::PendingInterval::get();389			<PendingUnstake<T>>::insert(390				(&staker_id, block),391				<PendingUnstake<T>>::get((&staker_id, block))392					.checked_add(&total_staked)393					.ok_or(ArithmeticError::Overflow)?,394			);395396			TotalStaked::<T>::set(397				TotalStaked::<T>::get()398					.checked_sub(&total_staked)399					.ok_or(ArithmeticError::Underflow)?,400			); // when error we should recover initial stake state for the staker401402			StakesPerAccount::<T>::remove(&staker_id);403404			Ok(None.into())405		}406407		#[pallet::weight(T::WeightInfo::sponsor_collection())]408		pub fn sponsor_collection(409			admin: OriginFor<T>,410			collection_id: CollectionId,411		) -> DispatchResult {412			let admin_id = ensure_signed(admin)?;413			ensure!(414				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,415				Error::<T>::NoPermission416			);417418			T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)419		}420		#[pallet::weight(T::WeightInfo::stop_sponsoring_collection())]421		pub fn stop_sponsoring_collection(422			admin: OriginFor<T>,423			collection_id: CollectionId,424		) -> DispatchResult {425			let admin_id = ensure_signed(admin)?;426427			ensure!(428				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,429				Error::<T>::NoPermission430			);431432			ensure!(433				T::CollectionHandler::get_sponsor(collection_id)?434					.ok_or(<Error<T>>::InvalidArgument)?435					== Self::account_id(),436				<Error<T>>::NoPermission437			);438			T::CollectionHandler::remove_collection_sponsor(collection_id)439		}440441		#[pallet::weight(T::WeightInfo::sponsor_contract())]442		pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {443			let admin_id = ensure_signed(admin)?;444445			ensure!(446				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,447				Error::<T>::NoPermission448			);449450			T::ContractHandler::set_sponsor(451				T::CrossAccountId::from_sub(Self::account_id()),452				contract_id,453			)454		}455456		#[pallet::weight(T::WeightInfo::stop_sponsoring_contract())]457		pub fn stop_sponsoring_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {458			let admin_id = ensure_signed(admin)?;459460			ensure!(461				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,462				Error::<T>::NoPermission463			);464465			ensure!(466				T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?467					== T::CrossAccountId::from_sub(Self::account_id()),468				<Error<T>>::NoPermission469			);470			T::ContractHandler::remove_contract_sponsor(contract_id)471		}472473		#[pallet::weight(0)]474		pub fn payout_stakers(admin: OriginFor<T>, stakers_number: Option<u8>) -> DispatchResult {475			let admin_id = ensure_signed(admin)?;476477			ensure!(478				admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,479				Error::<T>::NoPermission480			);481482			let current_recalc_block =483				Self::get_current_recalc_block(T::RelayBlockNumberProvider::current_block_number());484			let next_recalc_block = current_recalc_block + T::RecalculationInterval::get();485486			let mut storage_iterator = Self::get_next_calculated_key()487				.map_or(Staked::<T>::iter().skip(0), |key| {488					Staked::<T>::iter_from(key).skip(1)489				});490491			NextCalculatedRecord::<T>::set(None);492493			{494				let mut stakers_number = stakers_number.unwrap_or(20);495				let mut current_id = admin_id;496				let mut income_acc = BalanceOf::<T>::default();497498				while let Some(((id, staked_block), (amount, next_recalc_block_for_stake))) =499					storage_iterator.next()500				{501					if current_id != id {502						if income_acc != BalanceOf::<T>::default() {503							<T::Currency as Currency<T::AccountId>>::transfer(504								&T::TreasuryAccountId::get(),505								&current_id,506								income_acc,507								ExistenceRequirement::KeepAlive,508							)509							.and_then(|_| Self::add_lock_balance(&current_id, income_acc))?;510511							Self::deposit_event(Event::StakingRecalculation(512								current_id, amount, income_acc,513							));514						}515516						if stakers_number == 0 {517							NextCalculatedRecord::<T>::set(Some((id, staked_block)));518							break;519						}520						stakers_number -= 1;521						income_acc = BalanceOf::<T>::default();522						current_id = id;523					};524					if next_recalc_block_for_stake >= current_recalc_block {525						Self::recalculate_and_insert_stake(526							&current_id,527							staked_block,528							next_recalc_block,529							amount,530							((next_recalc_block_for_stake - current_recalc_block)531								/ T::RecalculationInterval::get())532							.into() + 1,533							&mut income_acc,534						);535					}536				}537			}538539			Ok(())540		}541	}542}543544impl<T: Config> Pallet<T> {545	pub fn account_id() -> T::AccountId {546		T::PalletId::get().into_account_truncating()547	}548549	fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {550		let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();551		locked_balance -= amount;552		Self::set_lock_unchecked(staker, locked_balance);553	}554555	fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {556		Self::get_locked_balance(staker)557			.map_or(<BalanceOf<T>>::default(), |l| l.amount)558			.checked_add(&amount)559			.map(|new_lock| Self::set_lock_unchecked(staker, new_lock))560			.ok_or(ArithmeticError::Overflow.into())561	}562563	fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {564		if amount.is_zero() {565			<T::Currency as LockableCurrency<T::AccountId>>::remove_lock(LOCK_IDENTIFIER, &staker);566		} else {567			<T::Currency as LockableCurrency<T::AccountId>>::set_lock(568				LOCK_IDENTIFIER,569				staker,570				amount,571				WithdrawReasons::all(),572			)573		}574	}575576	pub fn get_locked_balance(577		staker: impl EncodeLike<T::AccountId>,578	) -> Option<BalanceLock<BalanceOf<T>>> {579		<T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)580			.into_iter()581			.find(|l| l.id == LOCK_IDENTIFIER)582	}583584	pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {585		let staked = Staked::<T>::iter_prefix((staker,))586			.into_iter()587			.fold(<BalanceOf<T>>::default(), |acc, (_, (amount, _))| {588				acc + amount589			});590		if staked != <BalanceOf<T>>::default() {591			Some(staked)592		} else {593			None594		}595	}596597	pub fn total_staked_by_id_per_block(598		staker: impl EncodeLike<T::AccountId>,599	) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {600		let mut staked = Staked::<T>::iter_prefix((staker,))601			.into_iter()602			.map(|(block, (amount, _))| (block, amount))603			.collect::<Vec<_>>();604		staked.sort_by_key(|(block, _)| *block);605		if !staked.is_empty() {606			Some(staked)607		} else {608			None609		}610	}611612	pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {613		staker.map_or(Some(<TotalStaked<T>>::get()), |s| {614			Self::total_staked_by_id(s.as_sub())615		})616		// Self::total_staked_by_id(staker.as_sub())617	}618619	pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {620		Self::get_locked_balance(staker.as_sub())621			.map(|l| l.amount)622			.unwrap_or_default()623	}624625	pub fn cross_id_total_staked_per_block(626		staker: T::CrossAccountId,627	) -> Vec<(T::BlockNumber, BalanceOf<T>)> {628		Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()629	}630631	fn recalculate_and_insert_stake(632		staker: &T::AccountId,633		staked_block: T::BlockNumber,634		next_recalc_block: T::BlockNumber,635		base: BalanceOf<T>,636		iters: u32,637		income_acc: &mut BalanceOf<T>,638	) {639		let income = Self::calculate_income(base, iters);640641		base.checked_add(&income).map(|res| {642			<Staked<T>>::insert((staker, staked_block), (res, next_recalc_block));643			*income_acc += income;644		});645	}646647	fn calculate_income<I>(base: I, iters: u32) -> I648	where649		I: EncodeLike<BalanceOf<T>> + Balance,650	{651		let mut income = base;652653		(0..iters).for_each(|_| income += T::IntervalIncome::get() * income);654655		income - base656	}657658	fn get_current_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {659		(current_relay_block / T::RecalculationInterval::get()) * T::RecalculationInterval::get()660	}661662	// fn get_next_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {663	// 	Self::get_current_recalc_block(current_relay_block) + T::RecalculationInterval::get()664	// }665666	fn get_next_calculated_key() -> Option<Vec<u8>> {667		Self::get_next_calculated_record().map(|key| Staked::<T>::hashed_key_for(key))668	}669}670671impl<T: Config> Pallet<T>672where673	<<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum,674{675	pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {676		staker.map_or(PendingUnstake::<T>::iter_values().sum(), |s| {677			PendingUnstake::<T>::iter_prefix_values((s.as_sub(),)).sum()678		})679	}680681	pub fn cross_id_pending_unstake_per_block(682		staker: T::CrossAccountId,683	) -> Vec<(T::BlockNumber, BalanceOf<T>)> {684		let mut unsorted_res = PendingUnstake::<T>::iter_prefix((staker.as_sub(),))685			.into_iter()686			.collect::<Vec<_>>();687		unsorted_res.sort_by_key(|(block, _)| *block);688		unsorted_res689	}690}