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};40use codec::EncodeLike;41use pallet_balances::BalanceLock;42pub use types::ExtendedLockableCurrency;4344use frame_support::{45 dispatch::{DispatchResult},46 traits::{47 Currency, Get, LockableCurrency, WithdrawReasons, tokens::Balance, ExistenceRequirement,48 },49 ensure,50};5152use weights::WeightInfo;5354pub use pallet::*;55use pallet_evm::account::CrossAccountId;56use sp_runtime::{57 Perbill,58 traits::{BlockNumberProvider, CheckedAdd, CheckedSub},59 ArithmeticError,60};6162type BalanceOf<T> =63 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;6465const SECONDS_TO_BLOCK: u32 = 6;66const DAY: u32 = 60 * 60 * 24 / SECONDS_TO_BLOCK;67const WEEK: u32 = 7 * DAY;68const TWO_WEEK: u32 = 2 * WEEK;69const YEAR: u32 = DAY * 365;7071pub const LOCK_IDENTIFIER: [u8; 8] = *b"appstake";7273#[frame_support::pallet]74pub mod pallet {75 use super::*;76 use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};77 use frame_system::pallet_prelude::*;7879 #[pallet::config]80 pub trait Config: frame_system::Config + pallet_evm::account::Config {81 type Currency: ExtendedLockableCurrency<Self::AccountId>;8283 type TreasuryAccountId: Get<Self::AccountId>;8485 86 type WeightInfo: WeightInfo;8788 89 type BlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;9091 92 93 9495 96 97 }9899 #[pallet::pallet]100 #[pallet::generate_store(pub(super) trait Store)]101 pub struct Pallet<T>(_);102103 #[pallet::storage]104 pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;105106 #[pallet::storage]107 pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;108109 110 #[pallet::storage]111 pub type Staked<T: Config> = StorageNMap<112 Key = (113 Key<Blake2_128Concat, T::AccountId>,114 Key<Twox64Concat, T::BlockNumber>,115 ),116 Value = BalanceOf<T>,117 QueryKind = ValueQuery,118 >;119120 #[pallet::storage]121 pub type PendingUnstake<T: Config> = StorageNMap<122 Key = (123 Key<Blake2_128Concat, T::AccountId>,124 Key<Twox64Concat, T::BlockNumber>,125 ),126 Value = BalanceOf<T>,127 QueryKind = ValueQuery,128 >;129130 131 #[pallet::storage]132 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;133134 135 #[pallet::storage]136 #[pallet::getter(fn get_interest_block)]137 pub type NextInterestBlock<T: Config> =138 StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;139140 #[pallet::hooks]141 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {142 fn on_initialize(current_block: T::BlockNumber) -> Weight143 where144 <T as frame_system::Config>::BlockNumber: From<u32>,145 146 {147 PendingUnstake::<T>::iter()148 .filter_map(|((staker, block), amount)| {149 if block <= current_block {150 Some((staker, block, amount))151 } else {152 None153 }154 })155 .for_each(|(staker, block, amount)| {156 Self::unlock_balance_unchecked(&staker, amount); 157 <PendingUnstake<T>>::remove((staker, block));158 });159160 let next_interest_block = Self::get_interest_block();161162 if next_interest_block != 0.into() && current_block >= next_interest_block {163 let mut acc = <BalanceOf<T>>::default();164 let mut weight: Weight = 0;165 NextInterestBlock::<T>::set(current_block + DAY.into());166 Staked::<T>::iter()167 .filter(|((_, block), _)| *block + DAY.into() <= current_block)168 .for_each(|((staker, block), amount)| {169 Self::recalculate_stake(&staker, block, amount, &mut acc);170 171 });172 <TotalStaked<T>>::get()173 .checked_add(&acc)174 .map(|res| <TotalStaked<T>>::set(res));175 };176 0177 }178 }179180 #[pallet::call]181 impl<T: Config> Pallet<T> {182 #[pallet::weight(T::WeightInfo::set_admin_address())]183 pub fn set_admin_address(origin: OriginFor<T>, admin: T::AccountId) -> DispatchResult {184 ensure_root(origin)?;185 <Admin<T>>::set(Some(admin));186187 Ok(())188 }189190 #[pallet::weight(T::WeightInfo::start_app_promotion())]191 pub fn start_app_promotion(192 origin: OriginFor<T>,193 promotion_start_relay_block: T::BlockNumber,194 ) -> DispatchResult195 where196 <T as frame_system::Config>::BlockNumber: From<u32>,197 {198 ensure_root(origin)?;199200 201 if <StartBlock<T>>::get() == 0u32.into() {202 203 <StartBlock<T>>::set(promotion_start_relay_block);204205 <NextInterestBlock<T>>::set(promotion_start_relay_block + DAY.into());206 }207208 Ok(())209 }210211 #[pallet::weight(T::WeightInfo::stake())]212 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {213 let staker_id = ensure_signed(staker)?;214215 let balance =216 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);217218 ensure!(balance >= amount, ArithmeticError::Underflow);219220 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(221 &staker_id,222 amount,223 WithdrawReasons::all(),224 balance - amount,225 )?;226227 Self::add_lock_balance(&staker_id, amount)?;228229 let block_number = frame_system::Pallet::<T>::block_number();230231 <Staked<T>>::insert(232 (&staker_id, block_number),233 <Staked<T>>::get((&staker_id, block_number))234 .checked_add(&amount)235 .ok_or(ArithmeticError::Overflow)?,236 );237238 <TotalStaked<T>>::set(239 <TotalStaked<T>>::get()240 .checked_add(&amount)241 .ok_or(ArithmeticError::Overflow)?,242 );243244 Ok(())245 }246247 #[pallet::weight(T::WeightInfo::unstake())]248 pub fn unstake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {249 let staker_id = ensure_signed(staker)?;250251 let mut stakes = Staked::<T>::iter_prefix((&staker_id,)).collect::<Vec<_>>();252253 let total_staked = stakes254 .iter()255 .fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + *amount);256257 ensure!(total_staked >= amount, ArithmeticError::Underflow);258259 <TotalStaked<T>>::set(260 <TotalStaked<T>>::get()261 .checked_sub(&amount)262 .ok_or(ArithmeticError::Underflow)?,263 );264265 let block = frame_system::Pallet::<T>::block_number() + WEEK.into();266 <PendingUnstake<T>>::insert(267 (&staker_id, block),268 <PendingUnstake<T>>::get((&staker_id, block))269 .checked_add(&amount)270 .ok_or(ArithmeticError::Overflow)?,271 );272273 stakes.sort_by_key(|(block, _)| *block);274275 let mut acc_amount = amount;276 let new_state = stakes277 .into_iter()278 .map_while(|(block, balance_per_block)| {279 if acc_amount == <BalanceOf<T>>::default() {280 return None;281 }282 if acc_amount <= balance_per_block {283 let res = (block, balance_per_block - acc_amount, acc_amount);284 acc_amount = <BalanceOf<T>>::default();285 return Some(res);286 } else {287 acc_amount -= balance_per_block;288 return Some((block, <BalanceOf<T>>::default(), acc_amount));289 }290 })291 .collect::<Vec<_>>();292293 new_state294 .into_iter()295 .for_each(|(block, to_staked, _to_pending)| {296 if to_staked == <BalanceOf<T>>::default() {297 <Staked<T>>::remove((&staker_id, block));298 } else {299 <Staked<T>>::insert((&staker_id, block), to_staked);300 }301 });302303 Ok(())304 }305 }306}307308impl<T: Config> Pallet<T> {309 310 311312 313314 315316 317318 319 320 321 322 323 324325 326 327 328 329 330331 332 333334 335 336337 338 339 340341 342343 344 345 346 347 348349 350 351 352 353 354 355 356357 358359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376377 378 379 380 381 382 383 384 385 386387 388 389390 pub fn sponsor_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {391 Ok(())392 }393394 pub fn stop_sponsorign_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {395 Ok(())396 }397398 pub fn sponsor_conract(admin: T::AccountId, app_id: u32) -> DispatchResult {399 Ok(())400 }401402 pub fn stop_sponsorign_contract(admin: T::AccountId, app_id: u32) -> DispatchResult {403 Ok(())404 }405}406407impl<T: Config> Pallet<T> {408 fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {409 let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();410 locked_balance -= amount;411 Self::set_lock_unchecked(staker, locked_balance);412 }413414 fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {415 Self::get_locked_balance(staker)416 .map_or(<BalanceOf<T>>::default(), |l| l.amount)417 .checked_add(&amount)418 .map(|new_lock| Self::set_lock_unchecked(staker, new_lock))419 .ok_or(ArithmeticError::Overflow.into())420 }421422 fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {423 <T::Currency as LockableCurrency<T::AccountId>>::set_lock(424 LOCK_IDENTIFIER,425 staker,426 amount,427 WithdrawReasons::all(),428 )429 }430431 pub fn get_locked_balance(432 staker: impl EncodeLike<T::AccountId>,433 ) -> Option<BalanceLock<BalanceOf<T>>> {434 <T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)435 .into_iter()436 .find(|l| l.id == LOCK_IDENTIFIER)437 }438439 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {440 let staked = Staked::<T>::iter_prefix((staker,))441 .into_iter()442 .fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + amount);443 if staked != <BalanceOf<T>>::default() {444 Some(staked)445 } else {446 None447 }448 }449450 pub fn total_staked_by_id_per_block(451 staker: impl EncodeLike<T::AccountId>,452 ) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {453 let mut staked = Staked::<T>::iter_prefix((staker,))454 .into_iter()455 .map(|(block, amount)| (block, amount))456 .collect::<Vec<_>>();457 staked.sort_by_key(|(block, _)| *block);458 if !staked.is_empty() {459 Some(staked)460 } else {461 None462 }463 }464465 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {466 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {467 Self::total_staked_by_id(s.as_sub())468 })469 470 }471472 pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {473 Self::get_locked_balance(staker.as_sub())474 .map(|l| l.amount)475 .unwrap_or_default()476 }477478 pub fn cross_id_total_staked_per_block(479 staker: T::CrossAccountId,480 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {481 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()482 }483484 fn recalculate_stake(485 staker: &T::AccountId,486 block: T::BlockNumber,487 base: BalanceOf<T>,488 income_acc: &mut BalanceOf<T>,489 ) {490 let income = Self::calculate_income(base);491 base.checked_add(&income).map(|res| {492 <Staked<T>>::insert((staker, block), res);493 *income_acc += income;494 <T::Currency as Currency<T::AccountId>>::transfer(495 &T::TreasuryAccountId::get(),496 staker,497 income,498 ExistenceRequirement::KeepAlive,499 )500 .and_then(|_| Self::add_lock_balance(staker, income));501 });502 }503504 fn calculate_income<I>(base: I) -> I505 where506 I: EncodeLike<BalanceOf<T>> + Balance,507 {508 let day_rate = Perbill::from_rational(5u32, 1_0000);509 day_rate * base510 }511}512513impl<T: Config> Pallet<T>514where515 <<T as pallet::Config>::Currency as Currency<T::AccountId>>::Balance: Sum,516{517 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {518 staker.map_or(PendingUnstake::<T>::iter_values().sum(), |s| {519 PendingUnstake::<T>::iter_prefix_values((s.as_sub(),)).sum()520 })521 }522}