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 121 #[pallet::storage]122 pub type PendingUnstake<T: Config> = StorageNMap<123 Key = (124 Key<Blake2_128Concat, T::AccountId>,125 Key<Twox64Concat, T::BlockNumber>,126 ),127 Value = BalanceOf<T>,128 QueryKind = ValueQuery,129 >;130131 132 #[pallet::storage]133 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;134135 136 #[pallet::storage]137 #[pallet::getter(fn get_interest_block)]138 pub type NextInterestBlock<T: Config> =139 StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;140141 #[pallet::hooks]142 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {143 fn on_initialize(current_block: T::BlockNumber) -> Weight144 where145 <T as frame_system::Config>::BlockNumber: From<u32>,146 {147 let mut consumed_weight = 0;148 let mut add_weight = |reads, writes, weight| {149 consumed_weight += T::DbWeight::get().reads_writes(reads, writes);150 consumed_weight += weight;151 };152153 PendingUnstake::<T>::iter()154 .filter_map(|((staker, block), amount)| {155 if block <= current_block {156 Some((staker, block, amount))157 } else {158 None159 }160 })161 .for_each(|(staker, block, amount)| {162 Self::unlock_balance_unchecked(&staker, amount); 163 <PendingUnstake<T>>::remove((staker, block));164 });165166 let next_interest_block = Self::get_interest_block();167168 if next_interest_block != 0.into() && current_block >= next_interest_block {169 let mut acc = <BalanceOf<T>>::default();170171 NextInterestBlock::<T>::set(current_block + DAY.into());172 add_weight(0, 1, 0);173174 Staked::<T>::iter()175 .filter(|((_, block), _)| *block + DAY.into() <= current_block)176 .for_each(|((staker, block), amount)| {177 Self::recalculate_stake(&staker, block, amount, &mut acc);178 add_weight(0, 0, T::WeightInfo::recalculate_stake());179 });180 <TotalStaked<T>>::get()181 .checked_add(&acc)182 .map(|res| <TotalStaked<T>>::set(res));183 add_weight(0, 1, 0);184 };185 consumed_weight186 }187 }188189 #[pallet::call]190 impl<T: Config> Pallet<T> {191 #[pallet::weight(T::WeightInfo::set_admin_address())]192 pub fn set_admin_address(origin: OriginFor<T>, admin: T::AccountId) -> DispatchResult {193 ensure_root(origin)?;194 <Admin<T>>::set(Some(admin));195196 Ok(())197 }198199 #[pallet::weight(T::WeightInfo::start_app_promotion())]200 pub fn start_app_promotion(201 origin: OriginFor<T>,202 promotion_start_relay_block: T::BlockNumber,203 ) -> DispatchResult204 where205 <T as frame_system::Config>::BlockNumber: From<u32>,206 {207 ensure_root(origin)?;208209 210 if <StartBlock<T>>::get() == 0u32.into() {211 212 <StartBlock<T>>::set(promotion_start_relay_block);213214 <NextInterestBlock<T>>::set(promotion_start_relay_block + DAY.into());215 }216217 Ok(())218 }219220 #[pallet::weight(T::WeightInfo::stake())]221 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {222 let staker_id = ensure_signed(staker)?;223224 let balance =225 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);226227 ensure!(balance >= amount, ArithmeticError::Underflow);228229 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(230 &staker_id,231 amount,232 WithdrawReasons::all(),233 balance - amount,234 )?;235236 Self::add_lock_balance(&staker_id, amount)?;237238 let block_number = frame_system::Pallet::<T>::block_number();239240 <Staked<T>>::insert(241 (&staker_id, block_number),242 <Staked<T>>::get((&staker_id, block_number))243 .checked_add(&amount)244 .ok_or(ArithmeticError::Overflow)?,245 );246247 <TotalStaked<T>>::set(248 <TotalStaked<T>>::get()249 .checked_add(&amount)250 .ok_or(ArithmeticError::Overflow)?,251 );252253 Ok(())254 }255256 #[pallet::weight(T::WeightInfo::unstake())]257 pub fn unstake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {258 let staker_id = ensure_signed(staker)?;259260 let mut stakes = Staked::<T>::iter_prefix((&staker_id,)).collect::<Vec<_>>();261262 let total_staked = stakes263 .iter()264 .fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + *amount);265266 ensure!(total_staked >= amount, ArithmeticError::Underflow);267268 <TotalStaked<T>>::set(269 <TotalStaked<T>>::get()270 .checked_sub(&amount)271 .ok_or(ArithmeticError::Underflow)?,272 );273274 let block = frame_system::Pallet::<T>::block_number() + WEEK.into();275 <PendingUnstake<T>>::insert(276 (&staker_id, block),277 <PendingUnstake<T>>::get((&staker_id, block))278 .checked_add(&amount)279 .ok_or(ArithmeticError::Overflow)?,280 );281282 stakes.sort_by_key(|(block, _)| *block);283284 let mut acc_amount = amount;285 let new_state = stakes286 .into_iter()287 .map_while(|(block, balance_per_block)| {288 if acc_amount == <BalanceOf<T>>::default() {289 return None;290 }291 if acc_amount <= balance_per_block {292 let res = (block, balance_per_block - acc_amount, acc_amount);293 acc_amount = <BalanceOf<T>>::default();294 return Some(res);295 } else {296 acc_amount -= balance_per_block;297 return Some((block, <BalanceOf<T>>::default(), acc_amount));298 }299 })300 .collect::<Vec<_>>();301302 new_state303 .into_iter()304 .for_each(|(block, to_staked, _to_pending)| {305 if to_staked == <BalanceOf<T>>::default() {306 <Staked<T>>::remove((&staker_id, block));307 } else {308 <Staked<T>>::insert((&staker_id, block), to_staked);309 }310 });311312 Ok(())313 }314 }315}316317impl<T: Config> Pallet<T> {318 319 320321 322323 324325 326327 328 329 330 331 332 333334 335 336 337 338 339340 341 342343 344 345346 347 348 349350 351352 353 354 355 356 357358 359 360 361 362 363 364 365366 367368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385386 387 388 389 390 391 392 393 394 395396 397 398399 pub fn sponsor_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {400 Ok(())401 }402403 pub fn stop_sponsorign_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult {404 Ok(())405 }406407 pub fn sponsor_conract(admin: T::AccountId, app_id: u32) -> DispatchResult {408 Ok(())409 }410411 pub fn stop_sponsorign_contract(admin: T::AccountId, app_id: u32) -> DispatchResult {412 Ok(())413 }414}415416impl<T: Config> Pallet<T> {417 fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {418 let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();419 locked_balance -= amount;420 Self::set_lock_unchecked(staker, locked_balance);421 }422423 fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {424 Self::get_locked_balance(staker)425 .map_or(<BalanceOf<T>>::default(), |l| l.amount)426 .checked_add(&amount)427 .map(|new_lock| Self::set_lock_unchecked(staker, new_lock))428 .ok_or(ArithmeticError::Overflow.into())429 }430431 fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {432 <T::Currency as LockableCurrency<T::AccountId>>::set_lock(433 LOCK_IDENTIFIER,434 staker,435 amount,436 WithdrawReasons::all(),437 )438 }439440 pub fn get_locked_balance(441 staker: impl EncodeLike<T::AccountId>,442 ) -> Option<BalanceLock<BalanceOf<T>>> {443 <T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)444 .into_iter()445 .find(|l| l.id == LOCK_IDENTIFIER)446 }447448 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {449 let staked = Staked::<T>::iter_prefix((staker,))450 .into_iter()451 .fold(<BalanceOf<T>>::default(), |acc, (_, amount)| acc + amount);452 if staked != <BalanceOf<T>>::default() {453 Some(staked)454 } else {455 None456 }457 }458459 pub fn total_staked_by_id_per_block(460 staker: impl EncodeLike<T::AccountId>,461 ) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {462 let mut staked = Staked::<T>::iter_prefix((staker,))463 .into_iter()464 .map(|(block, amount)| (block, amount))465 .collect::<Vec<_>>();466 staked.sort_by_key(|(block, _)| *block);467 if !staked.is_empty() {468 Some(staked)469 } else {470 None471 }472 }473474 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {475 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {476 Self::total_staked_by_id(s.as_sub())477 })478 479 }480481 pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {482 Self::get_locked_balance(staker.as_sub())483 .map(|l| l.amount)484 .unwrap_or_default()485 }486487 pub fn cross_id_total_staked_per_block(488 staker: T::CrossAccountId,489 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {490 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()491 }492493 fn recalculate_stake(494 staker: &T::AccountId,495 block: T::BlockNumber,496 base: BalanceOf<T>,497 income_acc: &mut BalanceOf<T>,498 ) {499 let income = Self::calculate_income(base);500 base.checked_add(&income).map(|res| {501 <Staked<T>>::insert((staker, block), res);502 *income_acc += income;503 <T::Currency as Currency<T::AccountId>>::transfer(504 &T::TreasuryAccountId::get(),505 staker,506 income,507 ExistenceRequirement::KeepAlive,508 )509 .and_then(|_| Self::add_lock_balance(staker, income));510 });511 }512513 fn calculate_income<I>(base: I) -> I514 where515 I: EncodeLike<BalanceOf<T>> + Balance,516 {517 let day_rate = Perbill::from_rational(5u32, 1_0000);518 day_rate * base519 }520}521522impl<T: Config> Pallet<T>523where524 <<T as pallet::Config>::Currency as Currency<T::AccountId>>::Balance: Sum,525{526 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {527 staker.map_or(PendingUnstake::<T>::iter_values().sum(), |s| {528 PendingUnstake::<T>::iter_prefix_values((s.as_sub(),)).sum()529 })530 }531532 pub fn cross_id_pending_unstake_per_block(533 staker: T::CrossAccountId,534 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {535 let mut unsorted_res = PendingUnstake::<T>::iter_prefix((staker.as_sub(),))536 .into_iter()537 .collect::<Vec<_>>();538 unsorted_res.sort_by_key(|(block, _)| *block);539 unsorted_res540 }541}