123456789101112131415161718192021222324252627282930#![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::*;444546use 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;6869707172737475pub 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 97 #[pallet::constant]98 type PalletId: Get<PalletId>;99100 101 #[pallet::constant]102 type RecalculationInterval: Get<Self::BlockNumber>;103 104 #[pallet::constant]105 type PendingInterval: Get<Self::BlockNumber>;106107 108 #[pallet::constant]109 type Day: Get<Self::BlockNumber>; 110111 #[pallet::constant]112 type Nominal: Get<BalanceOf<Self>>;113114 #[pallet::constant]115 type IntervalIncome: Get<Perbill>;116117 118 type WeightInfo: WeightInfo;119120 121 type RelayBlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;122123 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 136 T::AccountId,137 138 BalanceOf<T>,139 140 BalanceOf<T>,141 ),142 }143144 #[pallet::error]145 pub enum Error<T> {146 147 AdminNotSet,148 149 NoPermission,150 151 NotSufficientFounds,152 153 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 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 >;172173 174 #[pallet::storage]175 pub type PendingUnstake<T: Config> = StorageNMap<176 Key = (177 Key<Blake2_128Concat, T::AccountId>,178 Key<Twox64Concat, T::BlockNumber>,179 ),180 Value = BalanceOf<T>,181 QueryKind = ValueQuery,182 >;183184 185 #[pallet::storage]186 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;187188 189 #[pallet::storage]190 #[pallet::getter(fn get_interest_block)]191 pub type NextInterestBlock<T: Config> =192 StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;193194 195 196 #[pallet::storage]197 #[pallet::getter(fn get_next_calculated_record)]198 pub type NextCalculatedRecord<T: Config> =199 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;200201 #[pallet::hooks]202 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {203 fn on_initialize(current_block: T::BlockNumber) -> Weight204 where205 <T as frame_system::Config>::BlockNumber: From<u32>,206 {207 let mut consumed_weight = 0;208 209 210 211 212213 PendingUnstake::<T>::iter()214 .filter_map(|((staker, block), amount)| {215 if block <= current_block {216 Some((staker, block, amount))217 } else {218 None219 }220 })221 .for_each(|(staker, block, amount)| {222 Self::unlock_balance_unchecked(&staker, amount); 223 <PendingUnstake<T>>::remove((staker, block));224 });225226 227 228 229 230 231232 233 234 235 236237 238 239 240 241 242 243 244 245 246 247 248 249250 251 252 253 254 255 consumed_weight256 }257 }258259 #[pallet::call]260 impl<T: Config> Pallet<T>261 where262 T::BlockNumber: From<u32> + Into<u32>,263 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum + From<u128>,264 {265 #[pallet::weight(T::WeightInfo::set_admin_address())]266 pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {267 ensure_root(origin)?;268 <Admin<T>>::set(Some(admin.as_sub().to_owned()));269270 Ok(())271 }272273 #[pallet::weight(T::WeightInfo::start_app_promotion())]274 pub fn start_app_promotion(275 origin: OriginFor<T>,276 promotion_start_relay_block: Option<T::BlockNumber>,277 ) -> DispatchResult278 where279 <T as frame_system::Config>::BlockNumber: From<u32>,280 {281 ensure_root(origin)?;282283 284 if <StartBlock<T>>::get() == 0u32.into() {285 let start_block = promotion_start_relay_block286 .unwrap_or(T::RelayBlockNumberProvider::current_block_number());287288 289 <StartBlock<T>>::set(start_block);290291 <NextInterestBlock<T>>::set(start_block + T::RecalculationInterval::get());292 }293294 Ok(())295 }296297 #[pallet::weight(T::WeightInfo::stop_app_promotion())]298 pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult299 where300 <T as frame_system::Config>::BlockNumber: From<u32>,301 {302 ensure_root(origin)?;303304 if <StartBlock<T>>::get() != 0u32.into() {305 <StartBlock<T>>::set(T::BlockNumber::default());306 <NextInterestBlock<T>>::set(T::BlockNumber::default());307 }308309 Ok(())310 }311312 #[pallet::weight(T::WeightInfo::stake())]313 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {314 let staker_id = ensure_signed(staker)?;315316 ensure!(317 amount >= Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get(),318 ArithmeticError::Underflow319 );320321 let balance =322 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);323324 ensure!(balance >= amount, ArithmeticError::Underflow);325326 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(327 &staker_id,328 amount,329 WithdrawReasons::all(),330 balance - amount,331 )?;332333 Self::add_lock_balance(&staker_id, amount)?;334335 let block_number = T::RelayBlockNumberProvider::current_block_number();336 let recalc_block = (block_number / T::RecalculationInterval::get() + 2u32.into())337 * T::RecalculationInterval::get();338339 <Staked<T>>::insert((&staker_id, block_number), {340 let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));341 balance_and_recalc_block.0 = balance_and_recalc_block342 .0343 .checked_add(&amount)344 .ok_or(ArithmeticError::Overflow)?;345 balance_and_recalc_block.1 = recalc_block;346 balance_and_recalc_block347 });348349 <TotalStaked<T>>::set(350 <TotalStaked<T>>::get()351 .checked_add(&amount)352 .ok_or(ArithmeticError::Overflow)?,353 );354355 Ok(())356 }357358 #[pallet::weight(T::WeightInfo::unstake())]359 pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {360 let staker_id = ensure_signed(staker)?;361362 let mut total_stakes = 0u64;363364 let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))365 .map(|(_, (amount, _))| {366 *&mut total_stakes += 1;367 amount368 })369 .sum();370371 let block =372 T::RelayBlockNumberProvider::current_block_number() + T::PendingInterval::get();373 <PendingUnstake<T>>::insert(374 (&staker_id, block),375 <PendingUnstake<T>>::get((&staker_id, block))376 .checked_add(&total_staked)377 .ok_or(ArithmeticError::Overflow)?,378 );379380 TotalStaked::<T>::set(381 TotalStaked::<T>::get()382 .checked_sub(&total_staked)383 .ok_or(ArithmeticError::Underflow)?,384 ); 385386 Ok(None.into())387388 389390 391392 393 394 395396 397398 399 400 401 402 403404 405 406 407 408 409 410 411 412413 414415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432433 434 435 436 437 438 439 440 441 442443 444 }445446 #[pallet::weight(T::WeightInfo::sponsor_collection())]447 pub fn sponsor_collection(448 admin: OriginFor<T>,449 collection_id: CollectionId,450 ) -> DispatchResult {451 let admin_id = ensure_signed(admin)?;452 ensure!(453 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,454 Error::<T>::NoPermission455 );456457 T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)458 }459 #[pallet::weight(T::WeightInfo::stop_sponsoring_collection())]460 pub fn stop_sponsoring_collection(461 admin: OriginFor<T>,462 collection_id: CollectionId,463 ) -> DispatchResult {464 let admin_id = ensure_signed(admin)?;465466 ensure!(467 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,468 Error::<T>::NoPermission469 );470471 ensure!(472 T::CollectionHandler::get_sponsor(collection_id)?473 .ok_or(<Error<T>>::InvalidArgument)?474 == Self::account_id(),475 <Error<T>>::NoPermission476 );477 T::CollectionHandler::remove_collection_sponsor(collection_id)478 }479480 #[pallet::weight(T::WeightInfo::sponsor_contract())]481 pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {482 let admin_id = ensure_signed(admin)?;483484 ensure!(485 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,486 Error::<T>::NoPermission487 );488489 T::ContractHandler::set_sponsor(490 T::CrossAccountId::from_sub(Self::account_id()),491 contract_id,492 )493 }494495 #[pallet::weight(T::WeightInfo::stop_sponsoring_contract())]496 pub fn stop_sponsoring_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {497 let admin_id = ensure_signed(admin)?;498499 ensure!(500 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,501 Error::<T>::NoPermission502 );503504 ensure!(505 T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?506 == T::CrossAccountId::from_sub(Self::account_id()),507 <Error<T>>::NoPermission508 );509 T::ContractHandler::remove_contract_sponsor(contract_id)510 }511512 #[pallet::weight(0)]513 pub fn payout_stakers(admin: OriginFor<T>, stakers_number: Option<u8>) -> DispatchResult {514 let admin_id = ensure_signed(admin)?;515516 ensure!(517 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,518 Error::<T>::NoPermission519 );520521 let current_recalc_block =522 Self::get_current_recalc_block(T::RelayBlockNumberProvider::current_block_number());523 let next_recalc_block = current_recalc_block + T::RecalculationInterval::get();524525 let mut storage_iterator = Self::get_next_calculated_key()526 .map_or(Staked::<T>::iter().skip(0), |key| {527 Staked::<T>::iter_from(key).skip(1)528 });529530 NextCalculatedRecord::<T>::set(None);531532 {533 let mut stakers_number = stakers_number.unwrap_or(20);534 let mut current_id = admin_id;535 let mut income_acc = BalanceOf::<T>::default();536537 while let Some(((id, staked_block), (amount, next_recalc_block_for_stake))) =538 storage_iterator.next()539 {540 if current_id != id {541 if income_acc != BalanceOf::<T>::default() {542 <T::Currency as Currency<T::AccountId>>::transfer(543 &T::TreasuryAccountId::get(),544 ¤t_id,545 income_acc,546 ExistenceRequirement::KeepAlive,547 )548 .and_then(|_| Self::add_lock_balance(¤t_id, income_acc))?;549550 Self::deposit_event(Event::StakingRecalculation(551 current_id, amount, income_acc,552 ));553 }554555 if stakers_number == 0 {556 NextCalculatedRecord::<T>::set(Some((id, staked_block)));557 break;558 }559 stakers_number -= 1;560 income_acc = BalanceOf::<T>::default();561 current_id = id;562 };563 if next_recalc_block_for_stake >= current_recalc_block {564 Self::recalculate_and_insert_stake(565 ¤t_id,566 staked_block,567 next_recalc_block,568 amount,569 ((next_recalc_block_for_stake - current_recalc_block)570 / T::RecalculationInterval::get())571 .into() + 1,572 &mut income_acc,573 );574 }575 }576 }577578 Ok(())579 }580 }581}582583impl<T: Config> Pallet<T> {584 pub fn account_id() -> T::AccountId {585 T::PalletId::get().into_account_truncating()586 }587588 fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {589 let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();590 locked_balance -= amount;591 Self::set_lock_unchecked(staker, locked_balance);592 }593594 fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {595 Self::get_locked_balance(staker)596 .map_or(<BalanceOf<T>>::default(), |l| l.amount)597 .checked_add(&amount)598 .map(|new_lock| Self::set_lock_unchecked(staker, new_lock))599 .ok_or(ArithmeticError::Overflow.into())600 }601602 fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {603 <T::Currency as LockableCurrency<T::AccountId>>::set_lock(604 LOCK_IDENTIFIER,605 staker,606 amount,607 WithdrawReasons::all(),608 )609 }610611 pub fn get_locked_balance(612 staker: impl EncodeLike<T::AccountId>,613 ) -> Option<BalanceLock<BalanceOf<T>>> {614 <T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)615 .into_iter()616 .find(|l| l.id == LOCK_IDENTIFIER)617 }618619 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {620 let staked = Staked::<T>::iter_prefix((staker,))621 .into_iter()622 .fold(<BalanceOf<T>>::default(), |acc, (_, (amount, _))| {623 acc + amount624 });625 if staked != <BalanceOf<T>>::default() {626 Some(staked)627 } else {628 None629 }630 }631632 pub fn total_staked_by_id_per_block(633 staker: impl EncodeLike<T::AccountId>,634 ) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {635 let mut staked = Staked::<T>::iter_prefix((staker,))636 .into_iter()637 .map(|(block, (amount, _))| (block, amount))638 .collect::<Vec<_>>();639 staked.sort_by_key(|(block, _)| *block);640 if !staked.is_empty() {641 Some(staked)642 } else {643 None644 }645 }646647 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {648 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {649 Self::total_staked_by_id(s.as_sub())650 })651 652 }653654 pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {655 Self::get_locked_balance(staker.as_sub())656 .map(|l| l.amount)657 .unwrap_or_default()658 }659660 pub fn cross_id_total_staked_per_block(661 staker: T::CrossAccountId,662 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {663 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()664 }665666 fn recalculate_and_insert_stake(667 staker: &T::AccountId,668 staked_block: T::BlockNumber,669 next_recalc_block: T::BlockNumber,670 base: BalanceOf<T>,671 iters: u32,672 income_acc: &mut BalanceOf<T>,673 ) {674 let income = Self::calculate_income(base, iters);675676 base.checked_add(&income).map(|res| {677 <Staked<T>>::insert((staker, staked_block), (res, next_recalc_block));678 *income_acc += income;679 });680 }681682 fn calculate_income<I>(base: I, iters: u32) -> I683 where684 I: EncodeLike<BalanceOf<T>> + Balance,685 {686 let mut income = base;687688 (0..iters).for_each(|_| income += T::IntervalIncome::get() * income);689690 income - base691 }692693 fn get_current_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {694 (current_relay_block / T::RecalculationInterval::get()) * T::RecalculationInterval::get()695 }696697 698 699 700701 fn get_next_calculated_key() -> Option<Vec<u8>> {702 Self::get_next_calculated_record().map(|key| Staked::<T>::hashed_key_for(key))703 }704}705706impl<T: Config> Pallet<T>707where708 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum,709{710 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {711 staker.map_or(PendingUnstake::<T>::iter_values().sum(), |s| {712 PendingUnstake::<T>::iter_prefix_values((s.as_sub(),)).sum()713 })714 }715716 pub fn cross_id_pending_unstake_per_block(717 staker: T::CrossAccountId,718 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {719 let mut unsorted_res = PendingUnstake::<T>::iter_prefix((staker.as_sub(),))720 .into_iter()721 .collect::<Vec<_>>();722 unsorted_res.sort_by_key(|(block, _)| *block);723 unsorted_res724 }725}