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 BalanceOf<T>,137 138 BalanceOf<T>,139 ),140 }141142 #[pallet::error]143 pub enum Error<T> {144 145 AdminNotSet,146 147 NoPermission,148 149 NotSufficientFounds,150 151 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 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 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 183 #[pallet::storage]184 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;185186 187 #[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); 214 <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 271 if <StartBlock<T>>::get() == 0u32.into() {272 let start_block = promotion_start_relay_block273 .unwrap_or(T::RelayBlockNumberProvider::current_block_number());274275 276 <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 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}