From 91047f4a381dc4b7a245224771ef28eeacd69e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dino=20Pa=C4=8Dandi?= <3002868+Dinonard@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:33:09 +0100 Subject: [PATCH] dApp Staking v3 - part 6 (#1093) * dApp staking v3 part 6 * Minor refactor of benchmarks * Weights integration * Fix * remove orig file * Minor refactoring, more benchmark code * Extract on_init logic * Some renaming * More benchmarks * Full benchmarks integration * Testing primitives * staking primitives * dev fix * Integration part1 * Integration part2 * Reward payout integration * Replace lock functionality with freeze * Cleanup TODOs * More negative tests * Frozen balance test * Zero div * Docs for inflation * Rename is_active & add some more docs * More docs * pallet docs * text * scripts * More tests * Test, docs * Review comment --- pallets/dapp-staking-v3/README.md | 30 +- .../dapp-staking-v3/coverage_extrinsics.sh | 11 + .../{coverage.sh => coverage_types.sh} | 0 .../{benchmarking.rs => benchmarking/mod.rs} | 380 ++++---- .../dapp-staking-v3/src/benchmarking/utils.rs | 218 +++++ pallets/dapp-staking-v3/src/dsv3_weight.rs | 100 --- pallets/dapp-staking-v3/src/lib.rs | 634 ++++++++------ pallets/dapp-staking-v3/src/test/mock.rs | 117 ++- .../dapp-staking-v3/src/test/testing_utils.rs | 48 +- pallets/dapp-staking-v3/src/test/tests.rs | 234 +++-- .../dapp-staking-v3/src/test/tests_types.rs | 162 ++-- pallets/dapp-staking-v3/src/types.rs | 89 +- pallets/dapp-staking-v3/src/weights.rs | 809 ++++++++++++++++++ pallets/inflation/src/lib.rs | 146 ++-- pallets/inflation/src/mock.rs | 7 +- primitives/src/dapp_staking.rs | 85 ++ primitives/src/lib.rs | 6 + primitives/src/testing.rs | 24 + runtime/local/src/lib.rs | 63 +- 19 files changed, 2189 insertions(+), 974 deletions(-) create mode 100755 pallets/dapp-staking-v3/coverage_extrinsics.sh rename pallets/dapp-staking-v3/{coverage.sh => coverage_types.sh} (100%) rename pallets/dapp-staking-v3/src/{benchmarking.rs => benchmarking/mod.rs} (74%) create mode 100644 pallets/dapp-staking-v3/src/benchmarking/utils.rs delete mode 100644 pallets/dapp-staking-v3/src/dsv3_weight.rs create mode 100644 pallets/dapp-staking-v3/src/weights.rs create mode 100644 primitives/src/dapp_staking.rs create mode 100644 primitives/src/testing.rs diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index afae398db2..72accbfc36 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -27,18 +27,22 @@ Each period consists of two subperiods: Each period is denoted by a number, which increments each time a new period begins. Period beginning is marked by the `voting` subperiod, after which follows the `build&earn` period. -Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to zero. This helps prevent projects remaining staked due to intertia. +Stakes are **only** valid throughout a period. When new period starts, all stakes are reset to **zero**. This helps prevent projects remaining staked due to intertia of stakers, and makes for a more dynamic staking system. + +Even though stakes are reset, locks (or freezes) of tokens remain. #### Voting -When `Voting` starts, all _stakes_ are reset to **zero**. +When `Voting` subperiod starts, all _stakes_ are reset to **zero**. Projects participating in dApp staking are expected to market themselves to (re)attract stakers. Stakers must assess whether the project they want to stake on brings value to the ecosystem, and then `vote` for it. Casting a vote, or staking, during the `Voting` subperiod makes the staker eligible for bonus rewards. so they are encouraged to participate. `Voting` subperiod length is expressed in _standard_ era lengths, even though the entire voting period is treated as a single _voting era_. -E.g. if `voting` subperiod lasts for **10 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **1000** blocks. +E.g. if `voting` subperiod lasts for **5 eras**, and each era lasts for **100** blocks, total length of the `voting` subperiod will be **500** blocks. +* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts Neither stakers nor dApps earn rewards during this subperiod - no new rewards are generated after `voting` subperiod ends. @@ -51,6 +55,16 @@ After each _era_ ends, eligible stakers and dApps can claim the rewards they ear It is still possible to _stake_ during this period, and stakers are encouraged to do so since this will increase the rewards they earn. The only exemption is the **final era** of the `build&earn` subperiod - it's not possible to _stake_ then since the stake would be invalid anyhow (stake is only valid from the next era which would be in the next period). +To continue the previous example where era length is **100** blocks, let's assume that `Build&Earn` subperiod lasts for 10 eras: +* Block 1, Era 1 starts, Period 1 starts, `Voting` subperiod starts +* Block 501, Era 2 starts, Period 1 continues, `Build&Earn` subperiod starts +* Block 601, Era 3 starts, Period 1 continues, `Build&Earn` subperiod continues +* Block 701, Era 4 starts, Period 1 continues, `Build&Earn` subperiod continues +* ... +* Block 1401, Era 11 starts, Period 1 continues, `Build&Earn` subperiod enters the final era +* Block 1501, Era 12 starts, Period 2 starts, `Voting` subperiod starts +* Block 2001, Era 13 starts, Period 2 continues, `Build&Earn` subperiod starts + ### dApps & Smart Contracts Protocol is called dApp staking, but internally it essentially works with smart contracts, or even more precise, smart contract addresses. @@ -63,12 +77,14 @@ Projects, or _dApps_, must be registered into protocol to participate. Only a privileged `ManagerOrigin` can perform dApp registration. The pallet itself does not make assumptions who the privileged origin is, and it can differ from runtime to runtime. +Once dApp has been registered, stakers can stake on it immediatelly. + +When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. + There is a limit of how many smart contracts can be registered at once. Once the limit is reached, any additional attempt to register a new contract will fail. #### Reward Beneficiary & Ownership -When contract is registered, it is assigned a unique compact numeric Id - 16 bit unsigned integer. This is important for the inner workings of the pallet, and is not directly exposed to the users. - After a dApp has been registered, it is possible to modify reward beneficiary or even the owner of the dApp. The owner can perform reward delegation and can further transfer ownership. #### Unregistration @@ -81,13 +97,13 @@ It's still possible however to claim past unclaimed rewards. Important to note that even if dApp has been unregistered, it still occupies a _slot_ in the dApp staking protocol and counts towards maximum number of registered dApps. -This will be improved in the future when dApp data will be cleaned up after the period ends. +This will be improved in the future when dApp data will be cleaned up after some time. ### Stakers #### Locking Tokens -In order for users to participate in dApp staking, the first step they need to take is lock some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). +In order for users to participate in dApp staking, the first step they need to take is lock (or freeze) some native currency. Reserved tokens cannot be locked, but tokens locked by another lock can be re-locked into dApp staking (double locked). **NOTE:** Locked funds cannot be used for paying fees, or for transfer. diff --git a/pallets/dapp-staking-v3/coverage_extrinsics.sh b/pallets/dapp-staking-v3/coverage_extrinsics.sh new file mode 100755 index 0000000000..6d0c77b3f0 --- /dev/null +++ b/pallets/dapp-staking-v3/coverage_extrinsics.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +targets=("register" "unregister" "set_dapp_reward_beneficiary" "set_dapp_owner" "maintenance_mode" \ + "lock" "unlock" "claim_unlocked" "relock_unlocking" \ + "stake" "unstake" "claim_staker_rewards" "claim_bonus_reward" "claim_dapp_reward" \ + "unstake_from_unregistered" "cleanup_expired_entries" "force" ) + +for target in "${targets[@]}" +do + cargo tarpaulin -p pallet-dapp-staking-v3 -o=html --output-dir=./coverage/$target -- test::tests::$target +done \ No newline at end of file diff --git a/pallets/dapp-staking-v3/coverage.sh b/pallets/dapp-staking-v3/coverage_types.sh similarity index 100% rename from pallets/dapp-staking-v3/coverage.sh rename to pallets/dapp-staking-v3/coverage_types.sh diff --git a/pallets/dapp-staking-v3/src/benchmarking.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs similarity index 74% rename from pallets/dapp-staking-v3/src/benchmarking.rs rename to pallets/dapp-staking-v3/src/benchmarking/mod.rs index 2b04874516..4f9cbafdb9 100644 --- a/pallets/dapp-staking-v3/src/benchmarking.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -26,161 +26,8 @@ use frame_system::{Pallet as System, RawOrigin}; use ::assert_matches::assert_matches; -// TODO: make benchmark utils file and move all these helper methods there to keep this file clean(er) - -// TODO2: non-extrinsic calls still need to be benchmarked. - -/// Run to the specified block number. -/// Function assumes first block has been initialized. -fn run_to_block(n: BlockNumberFor) { - while System::::block_number() < n { - DappStaking::::on_finalize(System::::block_number()); - System::::set_block_number(System::::block_number() + One::one()); - // This is performed outside of dapps staking but we expect it before on_initialize - DappStaking::::on_initialize(System::::block_number()); - } -} - -/// Run for the specified number of blocks. -/// Function assumes first block has been initialized. -fn run_for_blocks(n: BlockNumberFor) { - run_to_block::(System::::block_number() + n); -} - -/// Advance blocks until the specified era has been reached. -/// -/// Function has no effect if era is already passed. -pub(crate) fn advance_to_era(era: EraNumber) { - assert!(era >= ActiveProtocolState::::get().era); - while ActiveProtocolState::::get().era < era { - run_for_blocks::(One::one()); - } -} - -/// Advance blocks until next era has been reached. -pub(crate) fn advance_to_next_era() { - advance_to_era::(ActiveProtocolState::::get().era + 1); -} - -/// Advance blocks until the specified period has been reached. -/// -/// Function has no effect if period is already passed. -pub(crate) fn advance_to_period(period: PeriodNumber) { - assert!(period >= ActiveProtocolState::::get().period_number()); - while ActiveProtocolState::::get().period_number() < period { - run_for_blocks::(One::one()); - } -} - -/// Advance blocks until next period has been reached. -pub(crate) fn advance_to_next_period() { - advance_to_period::(ActiveProtocolState::::get().period_number() + 1); -} - -/// Advance blocks until next period type has been reached. -pub(crate) fn advance_to_next_subperiod() { - let subperiod = ActiveProtocolState::::get().subperiod(); - while ActiveProtocolState::::get().subperiod() == subperiod { - run_for_blocks::(One::one()); - } -} - -// All our networks use 18 decimals for native currency so this should be fine. -const UNIT: Balance = 1_000_000_000_000_000_000; - -// Minimum amount that must be staked on a dApp to enter any tier -const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; - -const NUMBER_OF_SLOTS: u32 = 100; - -const SEED: u32 = 9000; - -/// Assert that the last event equals the provided one. -fn assert_last_event(generic_event: ::RuntimeEvent) { - frame_system::Pallet::::assert_last_event(generic_event.into()); -} - -// Return all dApp staking events from the event buffer. -fn dapp_staking_events() -> Vec> { - System::::events() - .into_iter() - .map(|r| r.event) - .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) - .collect::>() -} - -// TODO: make it more generic per runtime? -pub fn initial_config() { - let era_length = T::StandardEraLength::get(); - let voting_period_length_in_eras = T::StandardErasPerVotingSubperiod::get(); - - // Init protocol state - ActiveProtocolState::::put(ProtocolState { - era: 1, - next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + One::one(), - period_info: PeriodInfo { - number: 1, - subperiod: Subperiod::Voting, - subperiod_end_era: 2, - }, - maintenance: false, - }); - - // Init tier params - let tier_params = TierParameters:: { - reward_portion: BoundedVec::try_from(vec![ - Permill::from_percent(40), - Permill::from_percent(30), - Permill::from_percent(20), - Permill::from_percent(10), - ]) - .unwrap(), - slot_distribution: BoundedVec::try_from(vec![ - Permill::from_percent(10), - Permill::from_percent(20), - Permill::from_percent(30), - Permill::from_percent(40), - ]) - .unwrap(), - tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 100 * UNIT, - minimum_amount: 80 * UNIT, - }, - TierThreshold::DynamicTvlAmount { - amount: 50 * UNIT, - minimum_amount: 40 * UNIT, - }, - TierThreshold::DynamicTvlAmount { - amount: 20 * UNIT, - minimum_amount: 20 * UNIT, - }, - TierThreshold::FixedTvlAmount { - amount: MIN_TIER_THRESHOLD, - }, - ]) - .unwrap(), - }; - - // Init tier config, based on the initial params - let init_tier_config = TiersConfiguration:: { - number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), - slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), - reward_portion: tier_params.reward_portion.clone(), - tier_thresholds: tier_params.tier_thresholds.clone(), - }; - - assert!(tier_params.is_valid()); - assert!(init_tier_config.is_valid()); - - StaticTierParams::::put(tier_params); - TierConfig::::put(init_tier_config.clone()); - NextTierConfig::::put(init_tier_config); -} - -fn max_number_of_contracts() -> u32 { - T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() -} +mod utils; +use utils::*; #[benchmarks] mod benchmarks { @@ -312,7 +159,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); #[extrinsic_call] _(RawOrigin::Signed(staker.clone()), amount); @@ -340,7 +187,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() * 2; - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -358,39 +205,6 @@ mod benchmarks { ); } - // TODO: maybe this is not needed. Compare it after running benchmarks to the 'not-full' unlock - #[benchmark] - fn full_unlock() { - initial_config::(); - - let staker: T::AccountId = whitelisted_caller(); - let owner: T::AccountId = account("dapp_owner", 0, SEED); - let smart_contract = T::BenchmarkHelper::get_smart_contract(1); - assert_ok!(DappStaking::::register( - RawOrigin::Root.into(), - owner.clone().into(), - smart_contract.clone(), - )); - - let amount = T::MinimumLockedAmount::get() * 2; - T::Currency::make_free_balance_be(&staker, amount); - assert_ok!(DappStaking::::lock( - RawOrigin::Signed(staker.clone()).into(), - amount, - )); - - #[extrinsic_call] - unlock(RawOrigin::Signed(staker.clone()), amount); - - assert_last_event::( - Event::::Unlocking { - account: staker, - amount, - } - .into(), - ); - } - #[benchmark] fn claim_unlocked(x: Linear<0, { T::MaxNumberOfStakedContracts::get() }>) { // Prepare staker account and lock some amount @@ -398,7 +212,7 @@ mod benchmarks { let amount = (T::MinimumStakeAmount::get() + 1) * Into::::into(max_number_of_contracts::()) + Into::::into(T::MaxUnlockingChunks::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -485,7 +299,7 @@ mod benchmarks { let amount = T::MinimumLockedAmount::get() * 2 + Into::::into(T::MaxUnlockingChunks::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -528,7 +342,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -565,7 +379,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() + 1; - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -612,7 +426,7 @@ mod benchmarks { // Lock some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -667,7 +481,7 @@ mod benchmarks { // Lock & stake some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -722,7 +536,7 @@ mod benchmarks { // Lock & stake some amount by the staker let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -761,7 +575,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get() * 1000 * UNIT; - T::Currency::make_free_balance_be(&owner, amount); + T::BenchmarkHelper::set_balance(&owner, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(owner.clone()).into(), amount, @@ -784,7 +598,7 @@ mod benchmarks { )); let staker: T::AccountId = account("staker", idx.into(), SEED); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -846,7 +660,7 @@ mod benchmarks { )); let amount = T::MinimumLockedAmount::get(); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -887,7 +701,7 @@ mod benchmarks { let staker: T::AccountId = whitelisted_caller(); let amount = T::MinimumLockedAmount::get() * Into::::into(T::MaxNumberOfStakedContracts::get()); - T::Currency::make_free_balance_be(&staker, amount); + T::BenchmarkHelper::set_balance(&staker, amount); assert_ok!(DappStaking::::lock( RawOrigin::Signed(staker.clone()).into(), amount, @@ -937,48 +751,143 @@ mod benchmarks { assert_last_event::(Event::::Force { forcing_type }.into()); } - // TODO: investigate why the PoV size is so large here, evne after removing read of `IntegratedDApps` storage. - // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs - // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise - // - the worst case measured is usually very far off the actual value that is consumed on chain. - // There's an ongoing item to improve it (mentioned on roundtable meeting). #[benchmark] - fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { - // Prepare init config (protocol state, tier params & config, etc.) + fn on_initialize_voting_to_build_and_earn() { initial_config::(); - let developer: T::AccountId = whitelisted_caller(); - for id in 0..x { - let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); - assert_ok!(DappStaking::::register( - RawOrigin::Root.into(), - developer.clone().into(), - smart_contract, - )); + let state = ActiveProtocolState::::get(); + assert_eq!(state.subperiod(), Subperiod::Voting, "Sanity check."); + + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + run_to_block::(state.next_era_start - 1); + DappStaking::::on_finalize(state.next_era_start - 1); + System::::set_block_number(state.next_era_start); + + #[block] + { + DappStaking::::era_and_period_handler(state.next_era_start, TierAssignment::Dummy); } - // TODO: try to make this more "shuffled" so the generated vector ends up being more random - let mut amount = 1000 * MIN_TIER_THRESHOLD; - for id in 0..x { - let staker = account("staker", id.into(), 1337); - T::Currency::make_free_balance_be(&staker, amount); - assert_ok!(DappStaking::::lock( - RawOrigin::Signed(staker.clone()).into(), - amount, - )); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + } - let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); - assert_ok!(DappStaking::::stake( - RawOrigin::Signed(staker.clone()).into(), - smart_contract, - amount, - )); + #[benchmark] + fn on_initialize_build_and_earn_to_voting() { + initial_config::(); + + // Get started + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting, + "Sanity check." + ); - // Slowly decrease the stake amount - amount.saturating_reduce(UNIT); + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + // Advance to build&earn subperiod + advance_to_next_subperiod::(); + let snapshot_state = ActiveProtocolState::::get(); + + // Advance over to the last era of the subperiod, and then again to the last block of that era. + advance_to_era::( + ActiveProtocolState::::get() + .period_info + .next_subperiod_start_era + - 1, + ); + run_to_block::(ActiveProtocolState::::get().next_era_start - 1); + + // Some sanity checks, we should still be in the build&earn subperiod, and in the first period. + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + + let new_era_start_block = ActiveProtocolState::::get().next_era_start; + DappStaking::::on_finalize(new_era_start_block - 1); + System::::set_block_number(new_era_start_block); + + #[block] + { + DappStaking::::era_and_period_handler(new_era_start_block, TierAssignment::Dummy); } - // Advance to next era + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::Voting + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number() + 1, + ); + } + + #[benchmark] + fn on_initialize_build_and_earn_to_build_and_earn() { + initial_config::(); + + // Register & stake contracts, just so we don't have empty stakes. + prepare_contracts_for_tier_assignment::(max_number_of_contracts::()); + + // Advance to build&earn subperiod + advance_to_next_subperiod::(); + let snapshot_state = ActiveProtocolState::::get(); + + // Advance over to the next era, and then again to the last block of that era. + advance_to_next_era::(); + run_to_block::(ActiveProtocolState::::get().next_era_start - 1); + + // Some sanity checks, we should still be in the build&earn subperiod, and in the first period. + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + + let new_era_start_block = ActiveProtocolState::::get().next_era_start; + DappStaking::::on_finalize(new_era_start_block - 1); + System::::set_block_number(new_era_start_block); + + #[block] + { + DappStaking::::era_and_period_handler(new_era_start_block, TierAssignment::Dummy); + } + + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn + ); + assert_eq!( + ActiveProtocolState::::get().period_number(), + snapshot_state.period_number(), + ); + } + + // Investigate why the PoV size is so large here, even after removing read of `IntegratedDApps` storage. + // Relevant file: polkadot-sdk/substrate/utils/frame/benchmarking-cli/src/pallet/writer.rs + // UPDATE: after some investigation, it seems that PoV size benchmarks are very unprecise + // - the worst case measured is usually very far off the actual value that is consumed on chain. + // There's an ongoing item to improve it (mentioned on roundtable meeting). + #[benchmark] + fn dapp_tier_assignment(x: Linear<0, { max_number_of_contracts::() }>) { + // Prepare init config (protocol state, tier params & config, etc.) + initial_config::(); + + // Register & stake contracts, to prepare for tier assignment. + prepare_contracts_for_tier_assignment::(x); advance_to_next_era::(); let reward_era = ActiveProtocolState::::get().era; @@ -987,9 +896,8 @@ mod benchmarks { #[block] { - let dapp_tiers = + let (dapp_tiers, _) = Pallet::::get_dapp_tier_assignment(reward_era, reward_period, reward_pool); - // TODO: how to move this outside of the 'block'? Cannot declare it outside, and then use it inside. assert_eq!(dapp_tiers.dapps.len(), x as usize); } } diff --git a/pallets/dapp-staking-v3/src/benchmarking/utils.rs b/pallets/dapp-staking-v3/src/benchmarking/utils.rs new file mode 100644 index 0000000000..688f964a76 --- /dev/null +++ b/pallets/dapp-staking-v3/src/benchmarking/utils.rs @@ -0,0 +1,218 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Pallet as DappStaking, *}; + +use astar_primitives::Balance; + +use frame_system::Pallet as System; + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(super) fn run_to_block(n: BlockNumberFor) { + while System::::block_number() < n { + DappStaking::::on_finalize(System::::block_number()); + System::::set_block_number(System::::block_number() + 1); + // This is performed outside of dapps staking but we expect it before on_initialize + DappStaking::::on_initialize(System::::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(super) fn run_for_blocks(n: BlockNumberFor) { + run_to_block::(System::::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(super) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next era has been reached. +pub(super) fn advance_to_next_era() { + advance_to_era::(ActiveProtocolState::::get().era + 1); +} + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(super) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks::(One::one()); + } +} + +/// Advance blocks until next period has been reached. +pub(super) fn advance_to_next_period() { + advance_to_period::(ActiveProtocolState::::get().period_number() + 1); +} + +/// Advance blocks until next period type has been reached. +pub(super) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks::(One::one()); + } +} + +/// All our networks use 18 decimals for native currency so this should be fine. +pub(super) const UNIT: Balance = 1_000_000_000_000_000_000; + +/// Minimum amount that must be staked on a dApp to enter any tier +pub(super) const MIN_TIER_THRESHOLD: Balance = 10 * UNIT; + +/// Number of slots in the tier system. +pub(super) const NUMBER_OF_SLOTS: u32 = 100; + +/// Random seed. +pub(super) const SEED: u32 = 9000; + +/// Assert that the last event equals the provided one. +pub(super) fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +// Return all dApp staking events from the event buffer. +pub(super) fn dapp_staking_events() -> Vec> { + System::::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| ::RuntimeEvent::from(e).try_into().ok()) + .collect::>() +} + +/// Initialize dApp staking pallet with initial config. +/// +/// **NOTE:** This assumes similar tier configuration for all runtimes. +/// If we decide to change this, we'll need to provide a more generic init function. +pub(super) fn initial_config() { + let era_length = T::CycleConfiguration::blocks_per_era(); + let voting_period_length_in_eras = T::CycleConfiguration::eras_per_voting_subperiod(); + + // Init protocol state + ActiveProtocolState::::put(ProtocolState { + era: 1, + next_era_start: era_length.saturating_mul(voting_period_length_in_eras.into()) + 1, + period_info: PeriodInfo { + number: 1, + subperiod: Subperiod::Voting, + next_subperiod_start_era: 2, + }, + maintenance: false, + }); + + // Init tier params + let tier_params = TierParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { + amount: 100 * UNIT, + minimum_amount: 80 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 50 * UNIT, + minimum_amount: 40 * UNIT, + }, + TierThreshold::DynamicTvlAmount { + amount: 20 * UNIT, + minimum_amount: 20 * UNIT, + }, + TierThreshold::FixedTvlAmount { + amount: MIN_TIER_THRESHOLD, + }, + ]) + .unwrap(), + }; + + // Init tier config, based on the initial params + let init_tier_config = TiersConfiguration:: { + number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: tier_params.reward_portion.clone(), + tier_thresholds: tier_params.tier_thresholds.clone(), + }; + + assert!(tier_params.is_valid()); + assert!(init_tier_config.is_valid()); + + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config.clone()); + NextTierConfig::::put(init_tier_config); +} + +/// Maximum number of contracts that 'makes sense' - considers both contract number limit & number of slots. +pub(super) fn max_number_of_contracts() -> u32 { + T::MaxNumberOfContracts::get().min(NUMBER_OF_SLOTS).into() +} + +/// Registers & staked on the specified number of smart contracts +/// +/// Stake amounts are decided in such a way to maximize tier filling rate. +/// This means that all of the contracts should end up in some tier. +pub(super) fn prepare_contracts_for_tier_assignment(x: u32) { + let developer: T::AccountId = whitelisted_caller(); + for id in 0..x { + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::register( + RawOrigin::Root.into(), + developer.clone().into(), + smart_contract, + )); + } + + // TODO: try to make this more "shuffled" so the generated vector ends up being more random + let mut amount = 1000 * MIN_TIER_THRESHOLD; + for id in 0..x { + let staker = account("staker", id.into(), 1337); + T::BenchmarkHelper::set_balance(&staker, amount); + assert_ok!(DappStaking::::lock( + RawOrigin::Signed(staker.clone()).into(), + amount, + )); + + let smart_contract = T::BenchmarkHelper::get_smart_contract(id as u32); + assert_ok!(DappStaking::::stake( + RawOrigin::Signed(staker.clone()).into(), + smart_contract, + amount, + )); + + // Slowly decrease the stake amount + amount.saturating_reduce(UNIT); + } +} diff --git a/pallets/dapp-staking-v3/src/dsv3_weight.rs b/pallets/dapp-staking-v3/src/dsv3_weight.rs deleted file mode 100644 index c1f88e9f1a..0000000000 --- a/pallets/dapp-staking-v3/src/dsv3_weight.rs +++ /dev/null @@ -1,100 +0,0 @@ - -// This file is part of Astar. - -// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later - -// Astar is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Astar is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Astar. If not, see . - -//! Autogenerated weights for pallet_dapp_staking_v3 -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Dinos-MBP`, CPU: `` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 - -// Executed Command: -// ./target/release/astar-collator -// benchmark -// pallet -// --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_dapp_staking-v3 -// --extrinsic=* -// --execution=wasm -// --wasm-execution=compiled -// --heap-pages=4096 -// --output=dapp_staking_v3.rs -// --template=./scripts/templates/weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for pallet_dapp_staking_v3. -pub trait WeightInfo { - fn dapp_tier_assignment(x: u32, ) -> Weight; -} - -/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) - /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 100]`. - fn dapp_tier_assignment(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(16_776_512, 3063) - // Standard Error: 3_400 - .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) - .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) - } -} - -// For backwards compatibility and tests -impl WeightInfo for () { - /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:0) - /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) - /// Storage: DappStaking ContractStake (r:101 w:0) - /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) - /// Storage: DappStaking TierConfig (r:1 w:0) - /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) - /// The range of component `x` is `[0, 100]`. - fn dapp_tier_assignment(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `449 + x * (33 ±0)` - // Estimated: `3063 + x * (2073 ±0)` - // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(16_776_512, 3063) - // Standard Error: 3_400 - .saturating_add(Weight::from_parts(2_636_298, 0).saturating_mul(x.into())) - .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) - } -} diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index e282f0959f..89ec801e53 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -17,32 +17,30 @@ // along with Astar. If not, see . //! # dApp Staking v3 Pallet -//! TODO //! -//! - [`Config`] +//! For detailed high level documentation, please refer to the attached README.md file. +//! The crate level docs will cover overal pallet structure & implementation details. //! //! ## Overview //! -//! Pallet that implements dapps staking protocol. +//! Pallet that implements the dApp staking v3 protocol. +//! It covers everything from locking, staking, tier configuration & assignment, reward calculation & payout. //! -//! <> +//! The `types` module contains all of the types used to implement the pallet. +//! All of these _types_ are exentisvely tested in their dedicated `test_types` module. //! -//! ## Interface -//! -//! ### Dispatchable Function -//! -//! <> -//! -//! ### Other -//! -//! <> +//! Rest of the pallet logic is concenrated in the lib.rs file. +//! This logic is tested in the `tests` module, with the help of extensive `testing_utils`. //! #![cfg_attr(not(feature = "std"), no_std)] use frame_support::{ pallet_prelude::*, - traits::{Currency, LockIdentifier, LockableCurrency, StorageVersion, WithdrawReasons}, + traits::{ + fungible::{Inspect as FunInspect, MutateFreeze as FunMutateFreeze}, + StorageVersion, + }, weights::Weight, }; use frame_system::pallet_prelude::*; @@ -52,7 +50,10 @@ use sp_runtime::{ }; pub use sp_std::vec::Vec; -use astar_primitives::Balance; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + Balance, BlockNumber, +}; pub use pallet::*; @@ -64,15 +65,23 @@ mod benchmarking; mod types; use types::*; -pub use types::{PriceProvider, RewardPoolProvider, TierThreshold}; +pub use types::{PriceProvider, TierThreshold}; -mod dsv3_weight; - -// Lock identifier for the dApp staking pallet -const STAKING_ID: LockIdentifier = *b"dapstake"; +pub mod weights; +pub use weights::WeightInfo; const LOG_TARGET: &str = "dapp-staking"; +/// Helper enum for benchmarking. +pub(crate) enum TierAssignment { + /// Real tier assignment calculation should be done. + Real, + /// Dummy tier assignment calculation should be done, e.g. default value should be returned. + #[cfg(feature = "runtime-benchmarks")] + Dummy, +} + +#[doc = include_str!("../README.md")] #[frame_support::pallet] pub mod pallet { use super::*; @@ -85,24 +94,27 @@ pub mod pallet { pub struct Pallet(_); #[cfg(feature = "runtime-benchmarks")] - pub trait BenchmarkHelper { + pub trait BenchmarkHelper { fn get_smart_contract(id: u32) -> SmartContract; + + fn set_balance(account: &AccountId, balance: Balance); } #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent> + TryInto>; + /// The overarching freeze reason. + type RuntimeFreezeReason: From; + /// Currency used for staking. - /// TODO: remove usage of deprecated LockableCurrency trait and use the new freeze approach. Might require some renaming of Lock to Freeze :) - // https://github.com/paritytech/substrate/pull/12951/ - // Look at nomination pools implementation for reference! - type Currency: LockableCurrency< + /// Reference: + type Currency: FunMutateFreeze< Self::AccountId, - Moment = Self::BlockNumber, + Id = Self::RuntimeFreezeReason, Balance = Balance, >; @@ -115,23 +127,11 @@ pub mod pallet { /// Used to provide price information about the native token. type NativePriceProvider: PriceProvider; - /// Used to provide reward pools amount. - type RewardPoolProvider: RewardPoolProvider; + /// Used to handle reward payouts & reward pool amount fetching. + type StakingRewardHandler: StakingRewardHandler; - /// Length of a standard era in block numbers. - #[pallet::constant] - type StandardEraLength: Get; - - /// Length of the `Voting` subperiod in standard eras. - /// Although `Voting` subperiod only consumes one 'era', we still measure its length in standard eras - /// for the sake of simplicity & consistency. - #[pallet::constant] - type StandardErasPerVotingSubperiod: Get; - - /// Length of the `Build&Earn` subperiod in standard eras. - /// Each `Build&Earn` subperiod consists of one or more distinct standard eras. - #[pallet::constant] - type StandardErasPerBuildAndEarnSubperiod: Get; + /// Describes era length, subperiods & period length, as well as cycle length. + type CycleConfiguration: CycleConfiguration; /// Maximum length of a single era reward span length entry. #[pallet::constant] @@ -159,7 +159,7 @@ pub mod pallet { #[pallet::constant] type UnlockingPeriod: Get; - /// Maximum amount of stake entries contract is allowed to have at once. + /// Maximum amount of stake contract entries an account is allowed to have at once. #[pallet::constant] type MaxNumberOfStakedContracts: Get; @@ -171,9 +171,12 @@ pub mod pallet { #[pallet::constant] type NumberOfTiers: Get; + /// Weight info for various calls & operations in the pallet. + type WeightInfo: WeightInfo; + /// Helper trait for benchmarks. #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper: BenchmarkHelper; + type BenchmarkHelper: BenchmarkHelper; } #[pallet::event] @@ -323,6 +326,8 @@ pub mod pallet { InternalUnstakeError, /// Rewards are no longer claimable since they are too old. RewardExpired, + /// Reward payout has failed due to an unexpected reason. + RewardPayoutFailed, /// There are no claimable rewards. NoClaimableRewards, /// An unexpected error occured while trying to claim staker rewards. @@ -350,14 +355,17 @@ pub mod pallet { /// General information about dApp staking protocol state. #[pallet::storage] - pub type ActiveProtocolState = - StorageValue<_, ProtocolState>, ValueQuery>; + #[pallet::whitelist_storage] + pub type ActiveProtocolState = StorageValue<_, ProtocolState, ValueQuery>; /// Counter for unique dApp identifiers. #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; /// Map of all dApps integrated into dApp staking protocol. + /// + /// Even though dApp is integrated, it does not mean it's still actively participating in dApp staking. + /// It might have been unregistered at some point in history. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< Hasher = Blake2_128Concat, @@ -489,11 +497,13 @@ pub mod pallet { // Prepare initial protocol state let protocol_state = ProtocolState { era: 1, - next_era_start: Pallet::::blocks_per_voting_period() + 1_u32.into(), + next_era_start: Pallet::::blocks_per_voting_period() + .checked_add(1) + .expect("Must not overflow - especially not at genesis."), period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, }; @@ -507,158 +517,26 @@ pub mod pallet { } #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(now: BlockNumberFor) -> Weight { - let mut protocol_state = ActiveProtocolState::::get(); - - // We should not modify pallet storage while in maintenance mode. - // This is a safety measure, since maintenance mode is expected to be - // enabled in case some misbehavior or corrupted storage is detected. - if protocol_state.maintenance { - return T::DbWeight::get().reads(1); - } - - // Nothing to do if it's not new era - if !protocol_state.is_new_era(now) { - return T::DbWeight::get().reads(1); - } - - // At this point it's clear that an era change will happen - let mut era_info = CurrentEraInfo::::get(); - - let current_era = protocol_state.era; - let next_era = current_era.saturating_add(1); - let (maybe_period_event, era_reward) = match protocol_state.subperiod() { - // Voting subperiod only lasts for one 'prolonged' era - Subperiod::Voting => { - // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. - let era_reward = EraReward { - staker_reward_pool: Balance::zero(), - staked: era_info.total_staked_amount(), - dapp_reward_pool: Balance::zero(), - }; - - let subperiod_end_era = - next_era.saturating_add(T::StandardErasPerBuildAndEarnSubperiod::get()); - let build_and_earn_start_block = - now.saturating_add(T::StandardEraLength::get()); - protocol_state - .advance_to_next_subperiod(subperiod_end_era, build_and_earn_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - - // Update tier configuration to be used when calculating rewards for the upcoming eras - let next_tier_config = NextTierConfig::::take(); - TierConfig::::put(next_tier_config); - - ( - Some(Event::::NewSubperiod { - subperiod: protocol_state.subperiod(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } - Subperiod::BuildAndEarn => { - let (staker_reward_pool, dapp_reward_pool) = - T::RewardPoolProvider::normal_reward_pools(); - let era_reward = EraReward { - staker_reward_pool, - staked: era_info.total_staked_amount(), - dapp_reward_pool, - }; - - // Distribute dapps into tiers, write it into storage - let dapp_tier_rewards = Self::get_dapp_tier_assignment( - current_era, - protocol_state.period_number(), - dapp_reward_pool, - ); - DAppTiers::::insert(¤t_era, dapp_tier_rewards); - - // Switch to `Voting` period if conditions are met. - if protocol_state.period_info.is_next_period(next_era) { - // Store info about period end - let bonus_reward_pool = T::RewardPoolProvider::bonus_reward_pool(); - PeriodEnd::::insert( - &protocol_state.period_number(), - PeriodEndInfo { - bonus_reward_pool, - total_vp_stake: era_info.staked_amount(Subperiod::Voting), - final_era: current_era, - }, - ); - - // For the sake of consistency we treat the whole `Voting` period as a single era. - // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. - let subperiod_end_era = next_era.saturating_add(1); - let voting_period_length = Self::blocks_per_voting_period(); - let next_era_start_block = now.saturating_add(voting_period_length); - - protocol_state - .advance_to_next_subperiod(subperiod_end_era, next_era_start_block); - - era_info.migrate_to_next_era(Some(protocol_state.subperiod())); - - // Re-calculate tier configuration for the upcoming new period - let tier_params = StaticTierParams::::get(); - let average_price = T::NativePriceProvider::average_price(); - let new_tier_config = - TierConfig::::get().calculate_new(average_price, &tier_params); - NextTierConfig::::put(new_tier_config); - - ( - Some(Event::::NewSubperiod { - subperiod: protocol_state.subperiod(), - number: protocol_state.period_number(), - }), - era_reward, - ) - } else { - let next_era_start_block = now.saturating_add(T::StandardEraLength::get()); - protocol_state.next_era_start = next_era_start_block; - - era_info.migrate_to_next_era(None); - - (None, era_reward) - } - } - }; - - // Update storage items - protocol_state.era = next_era; - ActiveProtocolState::::put(protocol_state); - - CurrentEraInfo::::put(era_info); - - let era_span_index = Self::era_reward_span_index(current_era); - let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); - if let Err(_) = span.push(current_era, era_reward) { - // This must never happen but we log the error just in case. - log::error!( - target: LOG_TARGET, - "Failed to push era {} into the era reward span.", - current_era - ); - } - EraRewards::::insert(&era_span_index, span); - - Self::deposit_event(Event::::NewEra { era: next_era }); - if let Some(period_event) = maybe_period_event { - Self::deposit_event(period_event); - } - - // TODO: benchmark later - T::DbWeight::get().reads_writes(3, 3) + impl Hooks for Pallet { + fn on_initialize(now: BlockNumber) -> Weight { + Self::era_and_period_handler(now, TierAssignment::Real) } } + /// A reason for freezing funds. + #[pallet::composite_enum] + pub enum FreezeReason { + /// Account is participating in dApp staking. + #[codec(index = 0)] + DAppStaking, + } + #[pallet::call] impl Pallet { /// Used to enable or disable maintenance mode. /// Can only be called by manager origin. #[pallet::call_index(0)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::maintenance_mode())] pub fn maintenance_mode(origin: OriginFor, enabled: bool) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; ActiveProtocolState::::mutate(|state| state.maintenance = enabled); @@ -672,7 +550,7 @@ pub mod pallet { /// If successful, smart contract will be assigned a simple, unique numerical identifier. /// Owner is set to be initial beneficiary & manager of the dApp. #[pallet::call_index(1)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::register())] pub fn register( origin: OriginFor, owner: T::AccountId, @@ -723,7 +601,7 @@ pub mod pallet { /// If set to `None`, rewards will be deposited to the dApp owner. /// After this call, all existing & future rewards will be paid out to the beneficiary. #[pallet::call_index(2)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::set_dapp_reward_beneficiary())] pub fn set_dapp_reward_beneficiary( origin: OriginFor, smart_contract: T::SmartContract, @@ -762,7 +640,7 @@ pub mod pallet { /// 1. when the dApp owner account is compromised, manager can change the owner to a new account /// 2. if project wants to transfer ownership to a new account (DAO, multisig, etc.). #[pallet::call_index(3)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::set_dapp_owner())] pub fn set_dapp_owner( origin: OriginFor, smart_contract: T::SmartContract, @@ -802,7 +680,7 @@ pub mod pallet { /// /// Can be called by dApp staking manager origin. #[pallet::call_index(4)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unregister())] pub fn unregister( origin: OriginFor, smart_contract: T::SmartContract, @@ -840,7 +718,7 @@ pub mod pallet { /// /// Locked amount can immediately be used for staking. #[pallet::call_index(5)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::lock())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -849,7 +727,7 @@ pub mod pallet { // Calculate & check amount available for locking let available_balance = - T::Currency::free_balance(&account).saturating_sub(ledger.active_locked_amount()); + T::Currency::balance(&account).saturating_sub(ledger.active_locked_amount()); let amount_to_lock = available_balance.min(amount); ensure!(!amount_to_lock.is_zero(), Error::::ZeroAmount); @@ -860,7 +738,7 @@ pub mod pallet { Error::::LockedAmountBelowThreshold ); - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.add_locked(amount_to_lock); }); @@ -879,7 +757,7 @@ pub mod pallet { /// If the amount is greater than the available amount for unlocking, everything is unlocked. /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. #[pallet::call_index(6)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unlock())] pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -917,7 +795,7 @@ pub mod pallet { .map_err(|_| Error::::TooManyUnlockingChunks)?; // Update storage - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_started(amount_to_unlock); }); @@ -932,8 +810,8 @@ pub mod pallet { /// Claims all of fully unlocked chunks, removing the lock from them. #[pallet::call_index(7)] - #[pallet::weight(Weight::zero())] - pub fn claim_unlocked(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))] + pub fn claim_unlocked(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -944,26 +822,25 @@ pub mod pallet { ensure!(amount > Zero::zero(), Error::::NoUnlockedChunksToClaim); // In case it's full unlock, account is exiting dApp staking, ensure all storage is cleaned up. - // TODO: will be used after benchmarks - let _removed_entries = if ledger.is_empty() { + let removed_entries = if ledger.is_empty() { let _ = StakerInfo::::clear_prefix(&account, ledger.contract_stake_count, None); ledger.contract_stake_count } else { 0 }; - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.unlocking_removed(amount); }); Self::deposit_event(Event::::ClaimedUnlocked { account, amount }); - Ok(()) + Ok(Some(T::WeightInfo::claim_unlocked(removed_entries)).into()) } #[pallet::call_index(8)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::relock_unlocking())] pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -980,7 +857,7 @@ pub mod pallet { Error::::LockedAmountBelowThreshold ); - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; CurrentEraInfo::::mutate(|era_info| { era_info.add_locked(amount); era_info.unlocking_removed(amount); @@ -1000,7 +877,7 @@ pub mod pallet { /// /// Staked amount is only eligible for rewards from the next era onwards. #[pallet::call_index(9)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::stake())] pub fn stake( origin: OriginFor, smart_contract: T::SmartContract, @@ -1013,7 +890,7 @@ pub mod pallet { let dapp_info = IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; - ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); + ensure!(dapp_info.is_registered(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let current_era = protocol_state.era; @@ -1104,7 +981,7 @@ pub mod pallet { // 5. // Update remaining storage entries - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; StakerInfo::::insert(&account, &smart_contract, new_staking_info); ContractStake::::insert(&dapp_info.id, contract_stake_info); @@ -1127,7 +1004,7 @@ pub mod pallet { /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced, /// and any spillover is subtracted from the `voting` amount. #[pallet::call_index(10)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unstake())] pub fn unstake( origin: OriginFor, smart_contract: T::SmartContract, @@ -1140,7 +1017,7 @@ pub mod pallet { let dapp_info = IntegratedDApps::::get(&smart_contract).ok_or(Error::::NotOperatedDApp)?; - ensure!(dapp_info.is_active(), Error::::NotOperatedDApp); + ensure!(dapp_info.is_registered(), Error::::NotOperatedDApp); let protocol_state = ActiveProtocolState::::get(); let current_era = protocol_state.era; @@ -1183,10 +1060,10 @@ pub mod pallet { ledger .unstake_amount(amount, current_era, protocol_state.period_info) .map_err(|err| match err { - // These are all defensive checks, which should never happen since we already checked them above. AccountLedgerError::InvalidPeriod | AccountLedgerError::InvalidEra => { Error::::UnclaimedRewards } + // This is a defensive check, which should never happen since we calculate the correct value above. AccountLedgerError::UnstakeAmountLargerThanStake => { Error::::UnstakeAmountTooLarge } @@ -1215,7 +1092,7 @@ pub mod pallet { StakerInfo::::insert(&account, &smart_contract, new_staking_info); } - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; Self::deposit_event(Event::::Unstake { account, @@ -1229,8 +1106,12 @@ pub mod pallet { /// Claims some staker rewards, if user has any. /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening. #[pallet::call_index(11)] - #[pallet::weight(Weight::zero())] - pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { + #[pallet::weight({ + let max_span_length = T::EraRewardSpanLength::get(); + T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length) + .max(T::WeightInfo::claim_staker_rewards_past_period(max_span_length)) + })] + pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1294,13 +1175,12 @@ pub mod pallet { rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); } + let rewards_len: u32 = rewards.len().unique_saturated_into(); - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - // Account exists since it has locked funds. - T::Currency::deposit_into_existing(&account, reward_sum) - .map_err(|_| Error::::InternalClaimStakerError)?; + T::StakingRewardHandler::payout_reward(&account, reward_sum) + .map_err(|_| Error::::RewardPayoutFailed)?; - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; rewards.into_iter().for_each(|(era, reward)| { Self::deposit_event(Event::::Reward { @@ -1310,12 +1190,17 @@ pub mod pallet { }); }); - Ok(()) + Ok(Some(if period_end.is_some() { + T::WeightInfo::claim_staker_rewards_past_period(rewards_len) + } else { + T::WeightInfo::claim_staker_rewards_ongoing_period(rewards_len) + }) + .into()) } /// Used to claim bonus reward for a smart contract, if eligible. #[pallet::call_index(12)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::claim_bonus_reward())] pub fn claim_bonus_reward( origin: OriginFor, smart_contract: T::SmartContract, @@ -1359,10 +1244,8 @@ pub mod pallet { Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake) * period_end_info.bonus_reward_pool; - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - // Account exists since it has locked funds. - T::Currency::deposit_into_existing(&account, bonus_reward) - .map_err(|_| Error::::InternalClaimStakerError)?; + T::StakingRewardHandler::payout_reward(&account, bonus_reward) + .map_err(|_| Error::::RewardPayoutFailed)?; // Cleanup entry since the reward has been claimed StakerInfo::::remove(&account, &smart_contract); @@ -1379,7 +1262,7 @@ pub mod pallet { /// Used to claim dApp reward for the specified era. #[pallet::call_index(13)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::claim_dapp_reward())] pub fn claim_dapp_reward( origin: OriginFor, smart_contract: T::SmartContract, @@ -1387,7 +1270,7 @@ pub mod pallet { ) -> DispatchResult { Self::ensure_pallet_enabled()?; - // TODO: Shall we make sure only dApp owner or beneficiary can trigger the claim? + // To keep in line with legacy behavior, dApp rewards can be claimed by anyone. let _ = ensure_signed(origin)?; let dapp_info = @@ -1400,7 +1283,7 @@ pub mod pallet { // 'Consume' dApp reward for the specified era, if possible. let mut dapp_tiers = DAppTiers::::get(&era).ok_or(Error::::NoDAppTierInfo)?; ensure!( - Self::oldest_claimable_period(dapp_tiers.period) <= protocol_state.period_number(), + dapp_tiers.period >= Self::oldest_claimable_period(protocol_state.period_number()), Error::::RewardExpired ); @@ -1415,8 +1298,8 @@ pub mod pallet { // Get reward destination, and deposit the reward. let beneficiary = dapp_info.reward_beneficiary(); - // TODO: add extra layer of security here to prevent excessive minting. Probably via Tokenomics2.0 pallet. - T::Currency::deposit_creating(beneficiary, amount); + T::StakingRewardHandler::payout_reward(&beneficiary, amount) + .map_err(|_| Error::::RewardPayoutFailed)?; // Write back updated struct to prevent double reward claims DAppTiers::::insert(&era, dapp_tiers); @@ -1435,7 +1318,7 @@ pub mod pallet { /// Used to unstake funds from a contract that was unregistered after an account staked on it. /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period. #[pallet::call_index(14)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::unstake_from_unregistered())] pub fn unstake_from_unregistered( origin: OriginFor, smart_contract: T::SmartContract, @@ -1444,7 +1327,7 @@ pub mod pallet { let account = ensure_signed(origin)?; ensure!( - !Self::is_active(&smart_contract), + !Self::is_registered(&smart_contract), Error::::ContractStillActive ); @@ -1485,11 +1368,8 @@ pub mod pallet { era_info.unstake_amount(amount, protocol_state.subperiod()); }); - // TODO: HOWEVER, we should not pay out bonus rewards for such contracts. - // Seems wrong because it serves as discentive for unstaking & moving over to a new contract. - // Update remaining storage entries - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; StakerInfo::::remove(&account, &smart_contract); Self::deposit_event(Event::::UnstakeFromUnregistered { @@ -1507,8 +1387,10 @@ pub mod pallet { /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. #[pallet::call_index(15)] - #[pallet::weight(Weight::zero())] - pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResult { + #[pallet::weight(T::WeightInfo::cleanup_expired_entries( + T::MaxNumberOfStakedContracts::get() + ))] + pub fn cleanup_expired_entries(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; let account = ensure_signed(origin)?; @@ -1544,14 +1426,17 @@ pub mod pallet { .contract_stake_count .saturating_reduce(entries_to_delete.unique_saturated_into()); ledger.maybe_cleanup_expired(threshold_period); // Not necessary but we do it for the sake of consistency - Self::update_ledger(&account, ledger); + Self::update_ledger(&account, ledger)?; Self::deposit_event(Event::::ExpiredEntriesRemoved { account, count: entries_to_delete.unique_saturated_into(), }); - Ok(()) + Ok(Some(T::WeightInfo::cleanup_expired_entries( + entries_to_delete.unique_saturated_into(), + )) + .into()) } /// Used to force a change of era or subperiod. @@ -1562,7 +1447,7 @@ pub mod pallet { /// /// Can only be called by manager origin. #[pallet::call_index(16)] - #[pallet::weight(Weight::zero())] + #[pallet::weight(T::WeightInfo::force())] pub fn force(origin: OriginFor, forcing_type: ForcingType) -> DispatchResult { Self::ensure_pallet_enabled()?; T::ManagerOrigin::ensure_origin(origin)?; @@ -1575,7 +1460,7 @@ pub mod pallet { match forcing_type { ForcingType::Era => (), ForcingType::Subperiod => { - state.period_info.subperiod_end_era = state.era.saturating_add(1); + state.period_info.next_subperiod_start_era = state.era.saturating_add(1); } } }); @@ -1609,34 +1494,41 @@ pub mod pallet { Ok(Some(who)) } - /// Update the account ledger, and dApp staking balance lock. + /// Update the account ledger, and dApp staking balance freeze. + /// + /// In case account ledger is empty, entries from the DB are removed and freeze is thawed. /// - /// In case account ledger is empty, entries from the DB are removed and lock is released. - pub(crate) fn update_ledger(account: &T::AccountId, ledger: AccountLedgerFor) { + /// This call can fail if the `freeze` or `thaw` operations fail. This should never happen since + /// runtime definition must ensure it supports necessary freezes. + pub(crate) fn update_ledger( + account: &T::AccountId, + ledger: AccountLedgerFor, + ) -> Result<(), DispatchError> { if ledger.is_empty() { Ledger::::remove(&account); - T::Currency::remove_lock(STAKING_ID, account); + T::Currency::thaw(&FreezeReason::DAppStaking.into(), account)?; } else { - T::Currency::set_lock( - STAKING_ID, + T::Currency::set_freeze( + &FreezeReason::DAppStaking.into(), account, ledger.active_locked_amount(), - WithdrawReasons::all(), - ); + )?; Ledger::::insert(account, ledger); } + + Ok(()) } /// Returns the number of blocks per voting period. - pub(crate) fn blocks_per_voting_period() -> BlockNumberFor { - T::StandardEraLength::get() - .saturating_mul(T::StandardErasPerVotingSubperiod::get().into()) + pub(crate) fn blocks_per_voting_period() -> BlockNumber { + T::CycleConfiguration::blocks_per_era() + .saturating_mul(T::CycleConfiguration::eras_per_voting_subperiod().into()) } - /// `true` if smart contract is active, `false` if it has been unregistered. - pub(crate) fn is_active(smart_contract: &T::SmartContract) -> bool { + /// `true` if smart contract is registered, `false` otherwise. + pub(crate) fn is_registered(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) - .map_or(false, |dapp_info| dapp_info.is_active()) + .map_or(false, |dapp_info| dapp_info.is_registered()) } /// Calculates the `EraRewardSpan` index for the specified era. @@ -1651,8 +1543,8 @@ pub mod pallet { } /// Unlocking period expressed in the number of blocks. - pub(crate) fn unlock_period() -> BlockNumberFor { - T::StandardEraLength::get().saturating_mul(T::UnlockingPeriod::get().into()) + pub(crate) fn unlock_period() -> BlockNumber { + T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into()) } /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. @@ -1668,7 +1560,7 @@ pub mod pallet { /// as well as the threshold for each tier. Threshold is the minimum amount of stake required to be eligible for a tier. /// Iterate over tier thresholds & capacities, starting from the top tier, and assign dApps to them. /// - /// ```ignore + /// ```text //// for each tier: /// for each unassigned dApp: /// if tier has capacity && dApp satisfies the tier threshold: @@ -1676,25 +1568,28 @@ pub mod pallet { /// else: /// exit loop since no more dApps will satisfy the threshold since they are sorted by score /// ``` + /// (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.) /// - /// 4. Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search. - /// - /// 5. Calculate rewards for each tier. + /// 4. Calculate rewards for each tier. /// This is done by dividing the total reward pool into tier reward pools, /// after which the tier reward pool is divided by the number of available slots in the tier. /// /// The returned object contains information about each dApp that made it into a tier. + /// Alongside tier assignment info, number of read DB contract stake entries is returned. pub(crate) fn get_dapp_tier_assignment( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, - ) -> DAppTierRewardsFor { + ) -> (DAppTierRewardsFor, DAppId) { let mut dapp_stakes = Vec::with_capacity(T::MaxNumberOfContracts::get() as usize); // 1. // Iterate over all staked dApps. // This is bounded by max amount of dApps we allow to be registered. + let mut counter = 0; for (dapp_id, stake_amount) in ContractStake::::iter() { + counter.saturating_inc(); + // Skip dApps which don't have ANY amount staked let stake_amount = match stake_amount.get(era, period) { Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, @@ -1744,13 +1639,12 @@ pub mod pallet { tier_id.saturating_inc(); } - // TODO: what if multiple dApps satisfy the tier entry threshold but there's not enough slots to accomodate them all? - - // 4. - // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). - dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); + // In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold, + // one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists. + // + // There is no explicit definition of which dApp gets the advantage - it's decided by dApp IDs hash & the unstable sort algorithm. - // 5. Calculate rewards. + // 4. Calculate rewards. let tier_rewards = tier_config .reward_portion .iter() @@ -1764,15 +1658,195 @@ pub mod pallet { }) .collect::>(); - // 6. + // 5. // Prepare and return tier & rewards info. // In case rewards creation fails, we just write the default value. This should never happen though. - DAppTierRewards::::new( - dapp_tiers, - tier_rewards, - period, + ( + DAppTierRewards::::new( + dapp_tiers, + tier_rewards, + period, + ) + .unwrap_or_default(), + counter, ) - .unwrap_or_default() + } + + /// Used to handle era & period transitions. + pub(crate) fn era_and_period_handler( + now: BlockNumber, + tier_assignment: TierAssignment, + ) -> Weight { + let mut protocol_state = ActiveProtocolState::::get(); + + // `ActiveProtocolState` is whitelisted, so we need to account for its read. + let mut consumed_weight = T::DbWeight::get().reads(1); + + // We should not modify pallet storage while in maintenance mode. + // This is a safety measure, since maintenance mode is expected to be + // enabled in case some misbehavior or corrupted storage is detected. + if protocol_state.maintenance { + return consumed_weight; + } + + // Nothing to do if it's not new era + if !protocol_state.is_new_era(now) { + return consumed_weight; + } + + // At this point it's clear that an era change will happen + let mut era_info = CurrentEraInfo::::get(); + + let current_era = protocol_state.era; + let next_era = current_era.saturating_add(1); + let (maybe_period_event, era_reward) = match protocol_state.subperiod() { + // Voting subperiod only lasts for one 'prolonged' era + Subperiod::Voting => { + // For the sake of consistency, we put zero reward into storage. There are no rewards for the voting subperiod. + let era_reward = EraReward { + staker_reward_pool: Balance::zero(), + staked: era_info.total_staked_amount(), + dapp_reward_pool: Balance::zero(), + }; + + let next_subperiod_start_era = next_era + .saturating_add(T::CycleConfiguration::eras_per_build_and_earn_subperiod()); + let build_and_earn_start_block = + now.saturating_add(T::CycleConfiguration::blocks_per_era()); + protocol_state.advance_to_next_subperiod( + next_subperiod_start_era, + build_and_earn_start_block, + ); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); + + // Update tier configuration to be used when calculating rewards for the upcoming eras + let next_tier_config = NextTierConfig::::take(); + TierConfig::::put(next_tier_config); + + consumed_weight + .saturating_accrue(T::WeightInfo::on_initialize_voting_to_build_and_earn()); + + ( + Some(Event::::NewSubperiod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } + Subperiod::BuildAndEarn => { + let staked = era_info.total_staked_amount(); + let (staker_reward_pool, dapp_reward_pool) = + T::StakingRewardHandler::staker_and_dapp_reward_pools(staked); + let era_reward = EraReward { + staker_reward_pool, + staked, + dapp_reward_pool, + }; + + // Distribute dapps into tiers, write it into storage + // + // To help with benchmarking, it's possible to omit real tier calculation using the `Dummy` approach. + // This must never be used in production code, obviously. + let (dapp_tier_rewards, counter) = match tier_assignment { + TierAssignment::Real => Self::get_dapp_tier_assignment( + current_era, + protocol_state.period_number(), + dapp_reward_pool, + ), + #[cfg(feature = "runtime-benchmarks")] + TierAssignment::Dummy => (DAppTierRewardsFor::::default(), 0), + }; + DAppTiers::::insert(¤t_era, dapp_tier_rewards); + + consumed_weight + .saturating_accrue(T::WeightInfo::dapp_tier_assignment(counter.into())); + + // Switch to `Voting` period if conditions are met. + if protocol_state.period_info.is_next_period(next_era) { + // Store info about period end + let bonus_reward_pool = T::StakingRewardHandler::bonus_reward_pool(); + PeriodEnd::::insert( + &protocol_state.period_number(), + PeriodEndInfo { + bonus_reward_pool, + total_vp_stake: era_info.staked_amount(Subperiod::Voting), + final_era: current_era, + }, + ); + + // For the sake of consistency we treat the whole `Voting` period as a single era. + // This means no special handling is required for this period, it only lasts potentially longer than a single standard era. + let next_subperiod_start_era = next_era.saturating_add(1); + let voting_period_length = Self::blocks_per_voting_period(); + let next_era_start_block = now.saturating_add(voting_period_length); + + protocol_state.advance_to_next_subperiod( + next_subperiod_start_era, + next_era_start_block, + ); + + era_info.migrate_to_next_era(Some(protocol_state.subperiod())); + + // Re-calculate tier configuration for the upcoming new period + let tier_params = StaticTierParams::::get(); + let average_price = T::NativePriceProvider::average_price(); + let new_tier_config = + TierConfig::::get().calculate_new(average_price, &tier_params); + NextTierConfig::::put(new_tier_config); + + consumed_weight.saturating_accrue( + T::WeightInfo::on_initialize_build_and_earn_to_voting(), + ); + + ( + Some(Event::::NewSubperiod { + subperiod: protocol_state.subperiod(), + number: protocol_state.period_number(), + }), + era_reward, + ) + } else { + let next_era_start_block = + now.saturating_add(T::CycleConfiguration::blocks_per_era()); + protocol_state.next_era_start = next_era_start_block; + + era_info.migrate_to_next_era(None); + + consumed_weight.saturating_accrue( + T::WeightInfo::on_initialize_build_and_earn_to_build_and_earn(), + ); + + (None, era_reward) + } + } + }; + + // Update storage items + protocol_state.era = next_era; + ActiveProtocolState::::put(protocol_state); + + CurrentEraInfo::::put(era_info); + + let era_span_index = Self::era_reward_span_index(current_era); + let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); + if let Err(_) = span.push(current_era, era_reward) { + // This must never happen but we log the error just in case. + log::error!( + target: LOG_TARGET, + "Failed to push era {} into the era reward span.", + current_era + ); + } + EraRewards::::insert(&era_span_index, span); + + Self::deposit_event(Event::::NewEra { era: next_era }); + if let Some(period_event) = maybe_period_event { + Self::deposit_event(period_event); + } + + consumed_weight } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 73535acd36..050eb112f2 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -24,7 +24,10 @@ use crate::{ use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU32, ConstU64}, + traits::{ + fungible::{Mutate as FunMutate, Unbalanced as FunUnbalanced}, + ConstU128, ConstU32, + }, weights::Weight, }; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -32,14 +35,14 @@ use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::{ - testing::Header, traits::{BlakeTwo256, IdentityLookup}, Permill, }; +use sp_std::cell::RefCell; + +use astar_primitives::{testing::Header, Balance, BlockNumber}; pub(crate) type AccountId = u64; -pub(crate) type BlockNumber = u64; -pub(crate) type Balance = u128; pub(crate) const EXISTENTIAL_DEPOSIT: Balance = 2; pub(crate) const MINIMUM_LOCK_AMOUNT: Balance = 10; @@ -61,7 +64,7 @@ construct_runtime!( ); parameter_types! { - pub const BlockHashCount: u64 = 250; + pub const BlockHashCount: BlockNumber = 250; pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); } @@ -103,9 +106,9 @@ impl pallet_balances::Config for Test { type ExistentialDeposit = ConstU128; type AccountStore = System; type HoldIdentifier = (); - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxFreezes = ConstU32<1>; type WeightInfo = (); } @@ -116,17 +119,31 @@ impl PriceProvider for DummyPriceProvider { } } -pub struct DummyRewardPoolProvider; -impl RewardPoolProvider for DummyRewardPoolProvider { - fn normal_reward_pools() -> (Balance, Balance) { +thread_local! { + pub(crate) static DOES_PAYOUT_SUCCEED: RefCell = RefCell::new(false); +} + +pub struct DummyStakingRewardHandler; +impl StakingRewardHandler for DummyStakingRewardHandler { + fn staker_and_dapp_reward_pools(_total_staked_value: Balance) -> (Balance, Balance) { ( Balance::from(1_000_000_000_000_u128), Balance::from(1_000_000_000_u128), ) } + fn bonus_reward_pool() -> Balance { Balance::from(3_000_000_u128) } + + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()> { + if DOES_PAYOUT_SUCCEED.with(|v| v.borrow().clone()) { + let _ = Balances::mint_into(beneficiary, reward); + Ok(()) + } else { + Err(()) + } + } } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] @@ -142,24 +159,49 @@ impl Default for MockSmartContract { } #[cfg(feature = "runtime-benchmarks")] -pub struct BenchmarkHelper(sp_std::marker::PhantomData); +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); #[cfg(feature = "runtime-benchmarks")] -impl crate::BenchmarkHelper for BenchmarkHelper { +impl crate::BenchmarkHelper + for BenchmarkHelper +{ fn get_smart_contract(id: u32) -> MockSmartContract { MockSmartContract::Wasm(id as AccountId) } + + fn set_balance(account: &AccountId, amount: Balance) { + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 16 + } + + fn blocks_per_era() -> u32 { + 10 + } } impl pallet_dapp_staking::Config for Test { type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; type Currency = Balances; type SmartContract = MockSmartContract; type ManagerOrigin = frame_system::EnsureRoot; type NativePriceProvider = DummyPriceProvider; - type RewardPoolProvider = DummyRewardPoolProvider; - type StandardEraLength = ConstU64<10>; - type StandardErasPerVotingSubperiod = ConstU32<8>; - type StandardErasPerBuildAndEarnSubperiod = ConstU32<16>; + type StakingRewardHandler = DummyStakingRewardHandler; + type CycleConfiguration = DummyCycleConfiguration; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<10>; @@ -169,13 +211,17 @@ impl pallet_dapp_staking::Config for Test { type MaxNumberOfStakedContracts = ConstU32<5>; type MinimumStakeAmount = ConstU128<3>; type NumberOfTiers = ConstU32<4>; + type WeightInfo = weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = BenchmarkHelper; + type BenchmarkHelper = BenchmarkHelper; } pub struct ExtBuilder; impl ExtBuilder { pub fn build() -> TestExternalities { + // Normal behavior is for reward payout to succeed + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + let mut storage = frame_system::GenesisConfig::default() .build_storage::() .unwrap(); @@ -194,12 +240,9 @@ impl ExtBuilder { ext.execute_with(|| { System::set_block_number(1); - // Not sure why the mess with type happens here, but trait specification is needed to compile - let era_length: BlockNumber = - <::StandardEraLength as sp_core::Get<_>>::get(); - let voting_period_length_in_eras: EraNumber = - <::StandardErasPerVotingSubperiod as sp_core::Get<_>>::get( - ); + let era_length = ::CycleConfiguration::blocks_per_era(); + let voting_period_length_in_eras = + ::CycleConfiguration::eras_per_voting_subperiod(); // Init protocol state pallet_dapp_staking::ActiveProtocolState::::put(ProtocolState { @@ -208,7 +251,7 @@ impl ExtBuilder { period_info: PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, }); @@ -227,7 +270,6 @@ impl ExtBuilder { era: 2, period: 1, }, - }); // Init tier params @@ -247,9 +289,18 @@ impl ExtBuilder { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { amount: 100, minimum_amount: 80 }, - TierThreshold::DynamicTvlAmount { amount: 50, minimum_amount: 40 }, - TierThreshold::DynamicTvlAmount { amount: 20, minimum_amount: 20 }, + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 80, + }, + TierThreshold::DynamicTvlAmount { + amount: 50, + minimum_amount: 40, + }, + TierThreshold::DynamicTvlAmount { + amount: 20, + minimum_amount: 20, + }, TierThreshold::FixedTvlAmount { amount: 15 }, ]) .unwrap(), @@ -267,10 +318,8 @@ impl ExtBuilder { pallet_dapp_staking::TierConfig::::put(init_tier_config.clone()); pallet_dapp_staking::NextTierConfig::::put(init_tier_config); - // TODO: include this into every test unless it explicitly doesn't need it. - // DappStaking::on_initialize(System::block_number()); - } - ); + DappStaking::on_initialize(System::block_number()); + }); ext } @@ -278,7 +327,7 @@ impl ExtBuilder { /// Run to the specified block number. /// Function assumes first block has been initialized. -pub(crate) fn run_to_block(n: u64) { +pub(crate) fn run_to_block(n: BlockNumber) { while System::block_number() < n { DappStaking::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); @@ -292,7 +341,7 @@ pub(crate) fn run_to_block(n: u64) { /// Run for the specified number of blocks. /// Function assumes first block has been initialized. -pub(crate) fn run_for_blocks(n: u64) { +pub(crate) fn run_for_blocks(n: BlockNumber) { run_to_block(System::block_number() + n); } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 14b3fa0bad..f20de522f1 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -19,20 +19,25 @@ use crate::test::mock::*; use crate::types::*; use crate::{ - pallet::Config, ActiveProtocolState, BlockNumberFor, ContractStake, CurrentEraInfo, DAppId, - DAppTiers, EraRewards, Event, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, PeriodEnd, - PeriodEndInfo, StakerInfo, TierConfig, + pallet::Config, ActiveProtocolState, ContractStake, CurrentEraInfo, DAppId, DAppTiers, + EraRewards, Event, FreezeReason, IntegratedDApps, Ledger, NextDAppId, NextTierConfig, + PeriodEnd, PeriodEndInfo, StakerInfo, TierConfig, }; -use frame_support::{assert_ok, traits::Get}; +use frame_support::{ + assert_ok, + traits::{fungible::InspectFreeze, Get}, +}; use sp_runtime::{traits::Zero, Perbill}; use std::collections::HashMap; +use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; + /// Helper struct used to store the entire pallet state snapshot. /// Used when comparison of before/after states is required. #[derive(Debug)] pub(crate) struct MemorySnapshot { - active_protocol_state: ProtocolState>, + active_protocol_state: ProtocolState, next_dapp_id: DAppId, current_era_info: EraInfo, integrated_dapps: HashMap< @@ -202,6 +207,8 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { let free_balance = Balances::free_balance(&account); let locked_balance = pre_snapshot.locked_balance(&account); + let init_frozen_balance = Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account); + let available_balance = free_balance .checked_sub(locked_balance) .expect("Locked amount cannot be greater than available free balance"); @@ -229,11 +236,17 @@ pub(crate) fn assert_lock(account: AccountId, amount: Balance) { pre_snapshot.current_era_info.total_locked + expected_lock_amount, "Total locked balance should be increased by the amount locked." ); + + assert_eq!( + init_frozen_balance + expected_lock_amount, + Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account) + ); } /// Start the unlocking process for locked funds and assert success. pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { let pre_snapshot = MemorySnapshot::new(); + let init_frozen_balance = Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account); assert!( pre_snapshot.ledger.contains_key(&account), @@ -310,6 +323,11 @@ pub(crate) fn assert_unlock(account: AccountId, amount: Balance) { .saturating_sub(expected_unlock_amount), post_era_info.total_locked ); + + assert_eq!( + init_frozen_balance - expected_unlock_amount, + Balances::balance_frozen(&FreezeReason::DAppStaking.into(), &account) + ); } /// Claims the unlocked funds back into free balance of the user and assert success. @@ -1133,7 +1151,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { let is_new_subperiod = pre_snapshot .active_protocol_state .period_info - .subperiod_end_era + .next_subperiod_start_era <= post_snapshot.active_protocol_state.era; // 1. Verify protocol state @@ -1149,15 +1167,15 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { "Voting subperiod only lasts for a single era." ); - let eras_per_bep: EraNumber = - ::StandardErasPerBuildAndEarnSubperiod::get(); + let eras_per_bep = + ::CycleConfiguration::eras_per_build_and_earn_subperiod(); assert_eq!( - post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.period_info.next_subperiod_start_era, post_protoc_state.era + eras_per_bep, "Build&earn must last for the predefined amount of standard eras." ); - let standard_era_length: BlockNumber = ::StandardEraLength::get(); + let standard_era_length = ::CycleConfiguration::blocks_per_era(); assert_eq!( post_protoc_state.next_era_start, current_block_number + standard_era_length, @@ -1177,15 +1195,15 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { "Ending 'Build&Earn' triggers a new period." ); assert_eq!( - post_protoc_state.period_info.subperiod_end_era, + post_protoc_state.period_info.next_subperiod_start_era, post_protoc_state.era + 1, "Voting era must last for a single era." ); - let blocks_per_standard_era: BlockNumber = - ::StandardEraLength::get(); - let eras_per_voting_subperiod: EraNumber = - ::StandardErasPerVotingSubperiod::get(); + let blocks_per_standard_era = + ::CycleConfiguration::blocks_per_era(); + let eras_per_voting_subperiod = + ::CycleConfiguration::eras_per_voting_subperiod(); let eras_per_voting_subperiod: BlockNumber = eras_per_voting_subperiod.into(); let era_length: BlockNumber = blocks_per_standard_era * eras_per_voting_subperiod; assert_eq!( diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 0795bd0653..1a53113ad6 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -18,7 +18,7 @@ use crate::test::{mock::*, testing_utils::*}; use crate::{ - pallet::Config, ActiveProtocolState, DAppId, EraNumber, EraRewards, Error, Event, ForcingType, + pallet::Config, ActiveProtocolState, DAppId, EraRewards, Error, Event, ForcingType, IntegratedDApps, Ledger, NextDAppId, PeriodNumber, StakerInfo, Subperiod, TierConfig, }; @@ -29,44 +29,7 @@ use frame_support::{ }; use sp_runtime::traits::Zero; -// TODO: remove this prior to the merge -#[test] -fn print_test() { - ExtBuilder::build().execute_with(|| { - use crate::dsv3_weight::WeightInfo; - println!( - ">>> dApp tier assignment reading & calculation {:?}", - crate::dsv3_weight::SubstrateWeight::::dapp_tier_assignment(100) - ); - - use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; - use scale_info::TypeInfo; - - #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] - struct RewardSize; - impl Get for RewardSize { - fn get() -> u32 { - 1_00_u32 - } - } - #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] - struct TierSize; - impl Get for TierSize { - fn get() -> u32 { - 4_u32 - } - } - println!( - ">>> Max encoded size for dapp tier rewards: {:?}", - crate::DAppTierRewards::::max_encoded_len() - ); - - println!( - ">>> Max encoded size of ContractStake: {:?}", - crate::ContractStakeAmount::max_encoded_len() - ); - }) -} +use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; #[test] fn maintenace_mode_works() { @@ -103,6 +66,7 @@ fn maintenace_mode_call_filtering_works() { assert_ok!(DappStaking::maintenance_mode(RuntimeOrigin::root(), true)); assert!(ActiveProtocolState::::get().maintenance); + assert_storage_noop!(DappStaking::on_initialize(1)); assert_noop!( DappStaking::register(RuntimeOrigin::root(), 1, MockSmartContract::Wasm(1)), Error::::Disabled @@ -234,9 +198,9 @@ fn on_initialize_base_state_change_works() { assert_eq!(protocol_state.period_number(), 1); // Advance eras just until we reach the next voting period - let eras_per_bep_period: EraNumber = - ::StandardErasPerBuildAndEarnSubperiod::get(); - let blocks_per_era: BlockNumber = ::StandardEraLength::get(); + let eras_per_bep_period = + ::CycleConfiguration::eras_per_build_and_earn_subperiod(); + let blocks_per_era: BlockNumber = ::CycleConfiguration::blocks_per_era(); for era in 2..(2 + eras_per_bep_period - 1) { let pre_block = System::block_number(); advance_to_next_era(); @@ -950,7 +914,7 @@ fn stake_in_final_era_fails() { // Force Build&Earn period ActiveProtocolState::::mutate(|state| { state.period_info.subperiod = Subperiod::BuildAndEarn; - state.period_info.subperiod_end_era = state.era + 1; + state.period_info.next_subperiod_start_era = state.era + 1; }); // Try to stake in the final era of the period, which should fail. @@ -962,7 +926,7 @@ fn stake_in_final_era_fails() { } #[test] -fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { +fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let smart_contract = MockSmartContract::default(); @@ -970,8 +934,17 @@ fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { assert_register(1, &smart_contract); assert_lock(account, 300); - // Stake some amount, then force next period + // Stake some amount, then force a few eras assert_stake(account, &smart_contract, 100); + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Stake must fail due to unclaimed rewards + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::UnclaimedRewards + ); + + // Should also fail in the next period advance_to_next_period(); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), @@ -980,6 +953,31 @@ fn stake_fails_if_unclaimed_rewards_from_past_eras_remain() { }) } +#[test] +fn stake_fails_if_claimable_bonus_rewards_from_past_remain() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + let account = 2; + assert_register(1, &smart_contract); + assert_lock(account, 300); + assert_stake(account, &smart_contract, 100); + + // Advance to next period, claim all staker rewards + advance_to_next_period(); + for _ in 0..required_number_of_reward_claims(account) { + assert_claim_staker_rewards(account); + } + + // Try to stake again on the same contract, expect an error due to unclaimed bonus rewards + advance_to_era(ActiveProtocolState::::get().era + 2); + assert_noop!( + DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), + Error::::UnclaimedRewards + ); + }) +} + #[test] fn stake_fails_if_not_enough_stakeable_funds_available() { ExtBuilder::build().execute_with(|| { @@ -1413,7 +1411,7 @@ fn claim_staker_rewards_after_expiry_fails() { advance_to_era( ActiveProtocolState::::get() .period_info - .subperiod_end_era + .next_subperiod_start_era - 1, ); assert_claim_staker_rewards(account); @@ -1438,6 +1436,34 @@ fn claim_staker_rewards_after_expiry_fails() { }) } +#[test] +fn claim_staker_rewards_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance into Build&Earn period, and allow one era to pass. + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_staker_rewards(RuntimeOrigin::signed(account)), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_staker_rewards(account); + }) +} + #[test] fn claim_bonus_reward_works() { ExtBuilder::build().execute_with(|| { @@ -1592,6 +1618,34 @@ fn claim_bonus_reward_after_expiry_fails() { }) } +#[test] +fn claim_bonus_reward_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance to next period so we can claim bonus reward + advance_to_next_period(); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_bonus_reward(RuntimeOrigin::signed(account), smart_contract), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_bonus_reward(account, &smart_contract); + }) +} + #[test] fn claim_dapp_reward_works() { ExtBuilder::build().execute_with(|| { @@ -1733,6 +1787,72 @@ fn claim_dapp_reward_twice_for_same_era_fails() { }) } +#[test] +fn claim_dapp_reward_for_expired_era_fails() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + let reward_retention_in_periods: PeriodNumber = + ::RewardRetentionInPeriods::get(); + + // Advance to period before the rewards expire. Claim reward must still work. + advance_to_period( + ActiveProtocolState::::get().period_number() + reward_retention_in_periods, + ); + assert_claim_dapp_reward(account, &smart_contract, 2); + + // Advance to the next era, expiring some rewards. + advance_to_next_period(); + assert_noop!( + DappStaking::claim_dapp_reward(RuntimeOrigin::signed(account), smart_contract, 3), + Error::::RewardExpired, + ); + }) +} + +#[test] +fn claim_dapp_reward_fails_due_to_payout_failure() { + ExtBuilder::build().execute_with(|| { + // Register smart contract, lock&stake some amount + let smart_contract = MockSmartContract::default(); + assert_register(1, &smart_contract); + + let account = 2; + let amount = 300; + assert_lock(account, amount); + assert_stake(account, &smart_contract, amount); + + // Advance 2 eras so we have an entry for reward claiming + advance_to_era(ActiveProtocolState::::get().era + 2); + + // Disable successfull reward payout + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = false); + assert_noop!( + DappStaking::claim_dapp_reward( + RuntimeOrigin::signed(account), + smart_contract, + ActiveProtocolState::::get().era - 1 + ), + Error::::RewardPayoutFailed, + ); + + // Re-enable it again, claim should work again + DOES_PAYOUT_SUCCEED.with(|v| *v.borrow_mut() = true); + assert_claim_dapp_reward( + account, + &smart_contract, + ActiveProtocolState::::get().era - 1, + ); + }) +} + #[test] fn unstake_from_unregistered_is_ok() { ExtBuilder::build().execute_with(|| { @@ -1924,8 +2044,8 @@ fn force_era_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), - init_state.period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), + init_state.next_subperiod_start_era(), ); // Go to the next block, and ensure new era is started @@ -1946,7 +2066,7 @@ fn force_era_works() { init_state.next_era_start > System::block_number() + 1, "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." ); - assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert!(init_state.next_subperiod_start_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Era)); System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { forcing_type: ForcingType::Era, @@ -1958,8 +2078,8 @@ fn force_era_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), - init_state.period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), + init_state.next_subperiod_start_era(), "Only era is bumped, but we don't expect to switch over to the next subperiod." ); @@ -2002,7 +2122,7 @@ fn force_subperiod_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), init_state.era + 1, "The switch to the next subperiod must happen in the next era." ); @@ -2027,7 +2147,7 @@ fn force_subperiod_works() { init_state.next_era_start > System::block_number() + 1, "Sanity check, new era cannot start in next block, otherwise the test doesn't guarantee it tests what's expected." ); - assert!(init_state.period_end_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); + assert!(init_state.next_subperiod_start_era() > init_state.era + 1, "Sanity check, otherwise the test doesn't guarantee it tests what's expected."); assert_ok!(DappStaking::force(RuntimeOrigin::root(), ForcingType::Subperiod)); System::assert_last_event(RuntimeEvent::DappStaking(Event::Force { forcing_type: ForcingType::Subperiod, @@ -2039,7 +2159,7 @@ fn force_subperiod_works() { System::block_number() + 1, ); assert_eq!( - ActiveProtocolState::::get().period_end_era(), + ActiveProtocolState::::get().next_subperiod_start_era(), init_state.era + 1, "The switch to the next subperiod must happen in the next era." ); @@ -2147,7 +2267,7 @@ fn get_dapp_tier_assignment_basic_example_works() { // Finally, the actual test let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let tier_assignment = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( protocol_state.era + 1, protocol_state.period_number(), dapp_reward_pool, @@ -2162,6 +2282,7 @@ fn get_dapp_tier_assignment_basic_example_works() { number_of_smart_contracts as usize - 1, "One contract doesn't make it into any tier." ); + assert_eq!(counter, number_of_smart_contracts); // 1st tier checks let (entry_1, entry_2) = (tier_assignment.dapps[0], tier_assignment.dapps[1]); @@ -2224,7 +2345,7 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { // Calculate tier assignment (we don't need dApps for this test) let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let tier_assignment = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( protocol_state.era, protocol_state.period_number(), dapp_reward_pool, @@ -2235,6 +2356,7 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { assert_eq!(tier_assignment.period, protocol_state.period_number()); assert_eq!(tier_assignment.rewards.len(), number_of_tiers as usize); assert!(tier_assignment.dapps.is_empty()); + assert!(counter.is_zero()); assert!( tier_assignment.rewards[0].is_zero(), diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 50074ec7a6..aadfbda6d1 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use astar_primitives::{Balance, BlockNumber}; +use astar_primitives::Balance; use frame_support::assert_ok; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::Permill; @@ -45,26 +45,26 @@ fn subperiod_sanity_check() { #[test] fn period_info_basic_checks() { let period_number = 2; - let subperiod_end_era = 5; + let next_subperiod_start_era = 5; let info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; // Sanity checks assert_eq!(info.number, period_number); assert_eq!(info.subperiod, Subperiod::Voting); - assert_eq!(info.subperiod_end_era, subperiod_end_era); + assert_eq!(info.next_subperiod_start_era, next_subperiod_start_era); // Voting period checks - assert!(!info.is_next_period(subperiod_end_era - 1)); - assert!(!info.is_next_period(subperiod_end_era)); - assert!(!info.is_next_period(subperiod_end_era + 1)); + assert!(!info.is_next_period(next_subperiod_start_era - 1)); + assert!(!info.is_next_period(next_subperiod_start_era)); + assert!(!info.is_next_period(next_subperiod_start_era + 1)); for era in vec![ - subperiod_end_era - 1, - subperiod_end_era, - subperiod_end_era + 1, + next_subperiod_start_era - 1, + next_subperiod_start_era, + next_subperiod_start_era + 1, ] { assert!( !info.is_next_period(era), @@ -76,16 +76,16 @@ fn period_info_basic_checks() { let info = PeriodInfo { number: period_number, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; - assert!(!info.is_next_period(subperiod_end_era - 1)); - assert!(info.is_next_period(subperiod_end_era)); - assert!(info.is_next_period(subperiod_end_era + 1)); + assert!(!info.is_next_period(next_subperiod_start_era - 1)); + assert!(info.is_next_period(next_subperiod_start_era)); + assert!(info.is_next_period(next_subperiod_start_era + 1)); } #[test] fn protocol_state_default() { - let protocol_state = ProtocolState::::default(); + let protocol_state = ProtocolState::default(); assert_eq!(protocol_state.era, 0); assert_eq!( @@ -96,14 +96,14 @@ fn protocol_state_default() { #[test] fn protocol_state_basic_checks() { - let mut protocol_state = ProtocolState::::default(); + let mut protocol_state = ProtocolState::default(); let period_number = 5; - let subperiod_end_era = 11; + let next_subperiod_start_era = 11; let next_era_start = 31; protocol_state.period_info = PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: subperiod_end_era, + next_subperiod_start_era: next_subperiod_start_era, }; protocol_state.next_era_start = next_era_start; @@ -116,30 +116,36 @@ fn protocol_state_basic_checks() { assert!(protocol_state.is_new_era(next_era_start + 1)); // Toggle new period type check - 'Voting' to 'BuildAndEarn' - let subperiod_end_era_1 = 23; + let next_subperiod_start_era_1 = 23; let next_era_start_1 = 41; - protocol_state.advance_to_next_subperiod(subperiod_end_era_1, next_era_start_1); + protocol_state.advance_to_next_subperiod(next_subperiod_start_era_1, next_era_start_1); assert_eq!(protocol_state.subperiod(), Subperiod::BuildAndEarn); assert_eq!( protocol_state.period_number(), period_number, "Switching from 'Voting' to 'BuildAndEarn' should not trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), subperiod_end_era_1); + assert_eq!( + protocol_state.next_subperiod_start_era(), + next_subperiod_start_era_1 + ); assert!(!protocol_state.is_new_era(next_era_start_1 - 1)); assert!(protocol_state.is_new_era(next_era_start_1)); // Toggle from 'BuildAndEarn' over to 'Voting' - let subperiod_end_era_2 = 24; + let next_subperiod_start_era_2 = 24; let next_era_start_2 = 91; - protocol_state.advance_to_next_subperiod(subperiod_end_era_2, next_era_start_2); + protocol_state.advance_to_next_subperiod(next_subperiod_start_era_2, next_era_start_2); assert_eq!(protocol_state.subperiod(), Subperiod::Voting); assert_eq!( protocol_state.period_number(), period_number + 1, "Switching from 'BuildAndEarn' to 'Voting' must trigger period bump." ); - assert_eq!(protocol_state.period_end_era(), subperiod_end_era_2); + assert_eq!( + protocol_state.next_subperiod_start_era(), + next_subperiod_start_era_2 + ); assert!(protocol_state.is_new_era(next_era_start_2)); } @@ -163,17 +169,17 @@ fn dapp_info_basic_checks() { dapp_info.reward_destination = Some(beneficiary); assert_eq!(*dapp_info.reward_beneficiary(), beneficiary); - // Check if dApp is active - assert!(dapp_info.is_active()); + // Check if dApp is registered + assert!(dapp_info.is_registered()); dapp_info.state = DAppState::Unregistered(10); - assert!(!dapp_info.is_active()); + assert!(!dapp_info.is_registered()); } #[test] fn unlocking_chunk_basic_check() { // Sanity check - let unlocking_chunk = UnlockingChunk::::default(); + let unlocking_chunk = UnlockingChunk::default(); assert!(unlocking_chunk.amount.is_zero()); assert!(unlocking_chunk.unlock_block.is_zero()); } @@ -181,7 +187,7 @@ fn unlocking_chunk_basic_check() { #[test] fn account_ledger_default() { get_u32_type!(UnlockingDummy, 5); - let acc_ledger = AccountLedger::::default(); + let acc_ledger = AccountLedger::::default(); assert!(acc_ledger.is_empty()); assert!(acc_ledger.active_locked_amount().is_zero()); @@ -190,7 +196,7 @@ fn account_ledger_default() { #[test] fn account_ledger_add_lock_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // First step, sanity checks assert!(acc_ledger.active_locked_amount().is_zero()); @@ -209,7 +215,7 @@ fn account_ledger_add_lock_amount_works() { #[test] fn account_ledger_subtract_lock_amount_basic_usage_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario // Cannot reduce if there is nothing locked, should be a noop @@ -250,10 +256,10 @@ fn account_ledger_subtract_lock_amount_basic_usage_works() { #[test] fn account_ledger_add_unlocking_chunk_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Base sanity check - let default_unlocking_chunk = UnlockingChunk::::default(); + let default_unlocking_chunk = UnlockingChunk::default(); assert!(default_unlocking_chunk.amount.is_zero()); assert!(default_unlocking_chunk.unlock_block.is_zero()); @@ -318,7 +324,7 @@ fn account_ledger_add_unlocking_chunk_works() { #[test] fn account_ledger_staked_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert!(acc_ledger.staked_amount(0).is_zero()); @@ -355,7 +361,7 @@ fn account_ledger_staked_amount_works() { #[test] fn account_ledger_staked_amount_for_type_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // 1st scenario - 'current' entry is set, 'future' is None let (voting_1, build_and_earn_1, period) = (31, 43, 2); @@ -416,7 +422,7 @@ fn account_ledger_staked_amount_for_type_works() { #[test] fn account_ledger_stakeable_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check for empty ledger assert!(acc_ledger.stakeable_amount(1).is_zero()); @@ -458,7 +464,7 @@ fn account_ledger_stakeable_amount_works() { #[test] fn account_ledger_staked_era_period_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); let (era_1, period) = (10, 2); let stake_amount_1 = StakeAmount { @@ -504,7 +510,7 @@ fn account_ledger_staked_era_period_works() { #[test] fn account_ledger_add_stake_amount_basic_example_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check let period_number = 2; @@ -515,7 +521,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { PeriodInfo { number: period_number, subperiod: Subperiod::Voting, - subperiod_end_era: 0 + next_subperiod_start_era: 0 } ) .is_ok()); @@ -528,7 +534,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 17; let stake_amount = 11; @@ -565,7 +571,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { let period_info_2 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let era_2 = era_1 + 1; assert!(acc_ledger.add_stake_amount(1, era_2, period_info_2).is_ok()); @@ -584,7 +590,7 @@ fn account_ledger_add_stake_amount_basic_example_works() { #[test] fn account_ledger_add_stake_amount_advanced_example_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // 1st scenario - stake some amount, and ensure values are as expected. let era_1 = 1; @@ -592,7 +598,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 17; let stake_amount_1 = 11; @@ -636,7 +642,7 @@ fn account_ledger_add_stake_amount_advanced_example_works() { #[test] fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let era_1 = 5; @@ -644,7 +650,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 13; let stake_amount = 7; @@ -667,7 +673,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -693,7 +699,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -703,7 +709,7 @@ fn account_ledger_add_stake_amount_invalid_era_or_period_fails() { #[test] fn account_ledger_add_stake_amount_too_large_amount_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check assert_eq!( @@ -713,7 +719,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { PeriodInfo { number: 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::UnavailableStakeFunds) @@ -725,7 +731,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; let lock_amount = 13; acc_ledger.add_lock_amount(lock_amount); @@ -747,7 +753,7 @@ fn account_ledger_add_stake_amount_too_large_amount_fails() { #[test] fn account_ledger_unstake_amount_basic_scenario_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 19; @@ -756,7 +762,7 @@ fn account_ledger_unstake_amount_basic_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -803,7 +809,7 @@ fn account_ledger_unstake_amount_basic_scenario_works() { #[test] fn account_ledger_unstake_amount_advanced_scenario_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 19; @@ -812,7 +818,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); @@ -885,7 +891,7 @@ fn account_ledger_unstake_amount_advanced_scenario_works() { #[test] fn account_ledger_unstake_from_invalid_era_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 13; @@ -894,7 +900,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -921,7 +927,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -947,7 +953,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { PeriodInfo { number: period_1 + 1, subperiod: Subperiod::Voting, - subperiod_end_era: 100 + next_subperiod_start_era: 100 } ), Err(AccountLedgerError::InvalidPeriod) @@ -957,7 +963,7 @@ fn account_ledger_unstake_from_invalid_era_fails() { #[test] fn account_ledger_unstake_too_much_fails() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Prep actions let amount_1 = 23; @@ -966,7 +972,7 @@ fn account_ledger_unstake_too_much_fails() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; acc_ledger.add_lock_amount(amount_1); assert!(acc_ledger @@ -982,7 +988,7 @@ fn account_ledger_unstake_too_much_fails() { #[test] fn account_ledger_unlockable_amount_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.unlockable_amount(0).is_zero()); @@ -999,7 +1005,7 @@ fn account_ledger_unlockable_amount_works() { let period_info = PeriodInfo { number: stake_period, subperiod: Subperiod::Voting, - subperiod_end_era: 100, + next_subperiod_start_era: 100, }; assert!(acc_ledger .add_stake_amount(stake_amount, lock_era, period_info) @@ -1023,7 +1029,7 @@ fn account_ledger_unlockable_amount_works() { #[test] fn account_ledger_claim_unlocked_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.claim_unlocked(0).is_zero()); @@ -1057,7 +1063,7 @@ fn account_ledger_claim_unlocked_works() { #[test] fn account_ledger_consume_unlocking_chunks_works() { get_u32_type!(UnlockingDummy, 5); - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); // Sanity check scenario assert!(acc_ledger.consume_unlocking_chunks().is_zero()); @@ -1076,7 +1082,7 @@ fn account_ledger_expired_cleanup_works() { get_u32_type!(UnlockingDummy, 5); // 1st scenario - nothing is expired - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1116,7 +1122,7 @@ fn account_ledger_claim_up_to_era_only_staked_without_cleanup_works() { let stake_era = 100; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1189,7 +1195,7 @@ fn account_ledger_claim_up_to_era_only_staked_with_cleanup_works() { let stake_era = 100; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1284,7 +1290,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_without_cleanup_works() { let stake_era = 50; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 5, build_and_earn: 11, @@ -1363,7 +1369,7 @@ fn account_ledger_claim_up_to_era_only_staked_future_with_cleanup_works() { let stake_era = 50; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 2, build_and_earn: 17, @@ -1468,7 +1474,7 @@ fn account_ledger_claim_up_to_era_staked_and_staked_future_works() { let stake_era_2 = stake_era_1 + 1; let acc_ledger_snapshot = { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 3, build_and_earn: 7, @@ -1561,7 +1567,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Only staked entry { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 2, build_and_earn: 17, @@ -1576,7 +1582,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Only staked-future entry { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked_future = Some(StakeAmount { voting: 2, build_and_earn: 17, @@ -1591,7 +1597,7 @@ fn account_ledger_claim_up_to_era_fails_for_historic_eras() { // Both staked and staked-future entries { - let mut acc_ledger = AccountLedger::::default(); + let mut acc_ledger = AccountLedger::::default(); acc_ledger.staked = StakeAmount { voting: 2, build_and_earn: 17, @@ -2240,7 +2246,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::Voting, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let amount_1 = 31; contract_stake.stake(amount_1, period_info_1, era_1); @@ -2268,7 +2274,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_1 = PeriodInfo { number: period_1, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; contract_stake.stake(amount_1, period_info_1, era_1); let entry_1_2 = contract_stake.get(stake_era_1, period_1).unwrap(); @@ -2310,7 +2316,7 @@ fn contract_stake_amount_stake_is_ok() { let period_info_2 = PeriodInfo { number: period_2, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let amount_3 = 41; @@ -2365,7 +2371,7 @@ fn contract_stake_amount_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::Voting, - subperiod_end_era: 20, + next_subperiod_start_era: 20, }; let stake_amount = 100; contract_stake.stake(stake_amount, period_info, era_1); @@ -2388,7 +2394,7 @@ fn contract_stake_amount_unstake_is_ok() { let period_info = PeriodInfo { number: period, subperiod: Subperiod::BuildAndEarn, - subperiod_end_era: 40, + next_subperiod_start_era: 40, }; let era_2 = era_1 + 1; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index b629a82774..3b18c7d7ac 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -65,21 +65,20 @@ //! use frame_support::{pallet_prelude::*, BoundedVec}; -use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, CheckedAdd, UniqueSaturatedInto, Zero}, + traits::{CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; pub use sp_std::{fmt::Debug, vec::Vec}; -use astar_primitives::Balance; +use astar_primitives::{Balance, BlockNumber}; use crate::pallet::Config; // Convenience type for `AccountLedger` usage. -pub type AccountLedgerFor = AccountLedger, ::MaxUnlockingChunks>; +pub type AccountLedgerFor = AccountLedger<::MaxUnlockingChunks>; // Convenience type for `DAppTierRewards` usage. pub type DAppTierRewardsFor = @@ -142,16 +141,16 @@ pub struct PeriodInfo { pub number: PeriodNumber, /// Subperiod type. pub subperiod: Subperiod, - /// Last era of the subperiod, after this a new subperiod should start. + /// Era in which the new subperiod starts. #[codec(compact)] - pub subperiod_end_era: EraNumber, + pub next_subperiod_start_era: EraNumber, } impl PeriodInfo { /// `true` if the provided era belongs to the next period, `false` otherwise. /// It's only possible to provide this information correctly for the ongoing `BuildAndEarn` subperiod. pub fn is_next_period(&self, era: EraNumber) -> bool { - self.subperiod == Subperiod::BuildAndEarn && self.subperiod_end_era <= era + self.subperiod == Subperiod::BuildAndEarn && self.next_subperiod_start_era <= era } } @@ -180,7 +179,7 @@ pub enum ForcingType { /// General information & state of the dApp staking protocol. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct ProtocolState { +pub struct ProtocolState { /// Ongoing era number. #[codec(compact)] pub era: EraNumber, @@ -193,28 +192,22 @@ pub struct ProtocolState { pub maintenance: bool, } -impl Default for ProtocolState -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, -{ +impl Default for ProtocolState { fn default() -> Self { Self { era: 0, - next_era_start: BlockNumber::from(1_u32), + next_era_start: 1, period_info: PeriodInfo { number: 0, subperiod: Subperiod::Voting, - subperiod_end_era: 2, + next_subperiod_start_era: 2, }, maintenance: false, } } } -impl ProtocolState -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen, -{ +impl ProtocolState { /// Current subperiod. pub fn subperiod(&self) -> Subperiod { self.period_info.subperiod @@ -226,8 +219,8 @@ where } /// Ending era of current period - pub fn period_end_era(&self) -> EraNumber { - self.period_info.subperiod_end_era + pub fn next_subperiod_start_era(&self) -> EraNumber { + self.period_info.next_subperiod_start_era } /// Checks whether a new era should be triggered, based on the provided _current_ block number argument @@ -239,7 +232,7 @@ where /// Triggers the next subperiod, updating appropriate parameters. pub fn advance_to_next_subperiod( &mut self, - subperiod_end_era: EraNumber, + next_subperiod_start_era: EraNumber, next_era_start: BlockNumber, ) { let period_number = if self.subperiod() == Subperiod::BuildAndEarn { @@ -251,7 +244,7 @@ where self.period_info = PeriodInfo { number: period_number, subperiod: self.subperiod().next(), - subperiod_end_era, + next_subperiod_start_era, }; self.next_era_start = next_era_start; } @@ -291,15 +284,15 @@ impl DAppInfo { } } - /// `true` if dApp is still active (registered), `false` otherwise. - pub fn is_active(&self) -> bool { + /// `true` if dApp is registered, `false` otherwise. + pub fn is_registered(&self) -> bool { self.state == DAppState::Registered } } /// How much was unlocked in some block. #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] -pub struct UnlockingChunk { +pub struct UnlockingChunk { /// Amount undergoing the unlocking period. #[codec(compact)] pub amount: Balance, @@ -308,10 +301,7 @@ pub struct UnlockingChunk Default for UnlockingChunk -where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy, -{ +impl Default for UnlockingChunk { fn default() -> Self { Self { amount: Balance::zero(), @@ -332,15 +322,12 @@ where TypeInfo, )] #[scale_info(skip_type_params(UnlockingLen))] -pub struct AccountLedger< - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, - UnlockingLen: Get, -> { +pub struct AccountLedger> { /// How much active locked amount an account has. This can be used for staking. #[codec(compact)] pub locked: Balance, /// Vector of all the unlocking chunks. This is also considered _locked_ but cannot be used for staking. - pub unlocking: BoundedVec, UnlockingLen>, + pub unlocking: BoundedVec, /// Primary field used to store how much was staked in a particular era. pub staked: StakeAmount, /// Secondary field used to store 'stake' information for the 'next era'. @@ -354,9 +341,8 @@ pub struct AccountLedger< pub contract_stake_count: u32, } -impl Default for AccountLedger +impl Default for AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { fn default() -> Self { @@ -370,9 +356,8 @@ where } } -impl AccountLedger +impl AccountLedger where - BlockNumber: AtLeast32BitUnsigned + MaxEncodedLen + Copy + Debug, UnlockingLen: Get, { /// Empty if no locked/unlocking/staked info exists. @@ -1367,9 +1352,6 @@ impl TierThreshold { Self::DynamicTvlAmount { amount, .. } => *amount, } } - - // TODO: maybe add a check that compares `Self` to another threshold and ensures it has lower requirements? - // Could be useful to have this check as a sanity check when params are configured. } /// Top level description of tier slot parameters used to calculate tier configuration. @@ -1500,7 +1482,8 @@ impl> TiersConfiguration { /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { - let new_number_of_slots = Self::calculate_number_of_slots(native_price); + // It must always be at least 1 slot. + let new_number_of_slots = Self::calculate_number_of_slots(native_price).max(1); // Calculate how much each tier gets slots. let new_slots_per_tier: Vec = params @@ -1652,7 +1635,9 @@ impl, NT: Get> DAppTierRewards { rewards: Vec, period: PeriodNumber, ) -> Result { - // TODO: should this part of the code ensure that dapps are sorted by Id? + // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). + let mut dapps = dapps; + dapps.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; @@ -1722,21 +1707,3 @@ pub trait PriceProvider { /// Get the price of the native token. fn average_price() -> FixedU64; } - -// TODO: however the implementation ends up looking, -// it should consider total staked amount when filling up the bonus pool. -// This is to ensure bonus rewards aren't too large in case there is little amount of staked funds. -pub trait RewardPoolProvider { - /// Get the reward pools for stakers and dApps. - /// - /// TODO: discussion about below - /// The assumption is that the underlying implementation keeps track of how often this is called. - /// E.g. let's assume it's supposed to be called at the end of each era. - /// In case era is forced, it will last shorter. If pallet is put into maintenance mode, era might last longer. - /// Reward should adjust to that accordingly. - /// Alternative is to provide number of blocks for which era lasted. - fn normal_reward_pools() -> (Balance, Balance); - - /// Get the bonus pool for stakers. - fn bonus_reward_pool() -> Balance; -} diff --git a/pallets/dapp-staking-v3/src/weights.rs b/pallets/dapp-staking-v3/src/weights.rs new file mode 100644 index 0000000000..d8891c7115 --- /dev/null +++ b/pallets/dapp-staking-v3/src/weights.rs @@ -0,0 +1,809 @@ + +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Autogenerated weights for pallet_dapp_staking_v3 +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-12-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Dinos-MacBook-Pro.local`, CPU: `` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/astar-collator +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_dapp_staking_v3 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=weights.rs +// --template=./scripts/templates/weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for pallet_dapp_staking_v3. +pub trait WeightInfo { + fn maintenance_mode() -> Weight; + fn register() -> Weight; + fn set_dapp_reward_beneficiary() -> Weight; + fn set_dapp_owner() -> Weight; + fn unregister() -> Weight; + fn lock() -> Weight; + fn unlock() -> Weight; + fn claim_unlocked(x: u32, ) -> Weight; + fn relock_unlocking() -> Weight; + fn stake() -> Weight; + fn unstake() -> Weight; + fn claim_staker_rewards_past_period(x: u32, ) -> Weight; + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight; + fn claim_bonus_reward() -> Weight; + fn claim_dapp_reward() -> Weight; + fn unstake_from_unregistered() -> Weight; + fn cleanup_expired_entries(x: u32, ) -> Weight; + fn force() -> Weight; + fn on_initialize_voting_to_build_and_earn() -> Weight; + fn on_initialize_build_and_earn_to_voting() -> Weight; + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight; + fn dapp_tier_assignment(x: u32, ) -> Weight; +} + +/// Weights for pallet_dapp_staking_v3 using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn maintenance_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn register() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3093` + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_reward_beneficiary() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(13_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(14_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:0 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + fn unregister() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3093) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn lock() -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `4764` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn unlock() -> Weight { + // Proof Size summary in bytes: + // Measured: `163` + // Estimated: `4764` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 3]`. + fn claim_unlocked(_x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `160 + x * (17 ±0)` + // Estimated: `4764` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(40_955_543, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn relock_unlocking() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `4764` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(40_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `4764` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake() -> Weight { + // Proof Size summary in bytes: + // Measured: `437` + // Estimated: `4764` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(54_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_past_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `485 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(51_664_058, 4764) + // Standard Error: 15_971 + .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `438 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(48_357_596, 4764) + // Standard Error: 16_408 + .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + fn claim_bonus_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `158` + // Estimated: `3603` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(36_000_000, 3603) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:1 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn claim_dapp_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `1086` + // Estimated: `3948` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(41_000_000, 3948) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake_from_unregistered() -> Weight { + // Proof Size summary in bytes: + // Measured: `397` + // Estimated: `4764` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 4764) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 3]`. + fn cleanup_expired_entries(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + x * (75 ±0)` + // Estimated: `4764 + x * (2613 ±0)` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(38_854_143, 4764) + // Standard Error: 54_134 + .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2613).saturating_mul(x.into())) + } + fn force() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:1 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:0 w:1) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_voting_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `3870` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking StaticTierParams (r:1 w:0) + /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:0 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:0 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_voting() -> Weight { + // Proof Size summary in bytes: + // Measured: `685` + // Estimated: `3870` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(37_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `73` + // Estimated: `3870` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(18_000_000, 3870) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `158 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(12_709_998, 3063) + // Standard Error: 8_047 + .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn maintenance_mode() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking CounterForIntegratedDApps (r:1 w:1) + /// Proof: DappStaking CounterForIntegratedDApps (max_values: Some(1), max_size: Some(4), added: 499, mode: MaxEncodedLen) + /// Storage: DappStaking NextDAppId (r:1 w:1) + /// Proof: DappStaking NextDAppId (max_values: Some(1), max_size: Some(2), added: 497, mode: MaxEncodedLen) + fn register() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3093` + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_reward_beneficiary() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(13_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + fn set_dapp_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 12_000_000 picoseconds. + Weight::from_parts(14_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:1) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:0 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + fn unregister() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `3093` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3093) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn lock() -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `4764` + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn unlock() -> Weight { + // Proof Size summary in bytes: + // Measured: `163` + // Estimated: `4764` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(43_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 3]`. + fn claim_unlocked(_x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `160 + x * (17 ±0)` + // Estimated: `4764` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(40_955_543, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + fn relock_unlocking() -> Weight { + // Proof Size summary in bytes: + // Measured: `174` + // Estimated: `4764` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(40_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn stake() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `4764` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking ContractStake (r:1 w:1) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake() -> Weight { + // Proof Size summary in bytes: + // Measured: `437` + // Estimated: `4764` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(54_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_past_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `485 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(51_664_058, 4764) + // Standard Error: 15_971 + .saturating_add(Weight::from_parts(4_243_613, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:0) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 8]`. + fn claim_staker_rewards_ongoing_period(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `438 + x * (8 ±0)` + // Estimated: `4764` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(48_357_596, 4764) + // Standard Error: 16_408 + .saturating_add(Weight::from_parts(4_302_059, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:1 w:0) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + fn claim_bonus_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `158` + // Estimated: `3603` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(36_000_000, 3603) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:1 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn claim_dapp_reward() -> Weight { + // Proof Size summary in bytes: + // Measured: `1086` + // Estimated: `3948` + // Minimum execution time: 37_000_000 picoseconds. + Weight::from_parts(41_000_000, 3948) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: DappStaking IntegratedDApps (r:1 w:0) + /// Proof: DappStaking IntegratedDApps (max_values: Some(65535), max_size: Some(123), added: 2103, mode: MaxEncodedLen) + /// Storage: DappStaking StakerInfo (r:1 w:1) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + fn unstake_from_unregistered() -> Weight { + // Proof Size summary in bytes: + // Measured: `397` + // Estimated: `4764` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 4764) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking StakerInfo (r:4 w:3) + /// Proof: DappStaking StakerInfo (max_values: None, max_size: Some(138), added: 2613, mode: MaxEncodedLen) + /// Storage: DappStaking Ledger (r:1 w:1) + /// Proof: DappStaking Ledger (max_values: None, max_size: Some(250), added: 2725, mode: MaxEncodedLen) + /// Storage: Balances Locks (r:1 w:1) + /// Proof: Balances Locks (max_values: None, max_size: Some(1299), added: 3774, mode: MaxEncodedLen) + /// Storage: Balances Freezes (r:1 w:0) + /// Proof: Balances Freezes (max_values: None, max_size: Some(49), added: 2524, mode: MaxEncodedLen) + /// The range of component `x` is `[1, 3]`. + fn cleanup_expired_entries(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `250 + x * (75 ±0)` + // Estimated: `4764 + x * (2613 ±0)` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(38_854_143, 4764) + // Standard Error: 54_134 + .saturating_add(Weight::from_parts(7_359_116, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2613).saturating_mul(x.into())) + } + fn force() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:1 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:0 w:1) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_voting_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `151` + // Estimated: `3870` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking StaticTierParams (r:1 w:0) + /// Proof: DappStaking StaticTierParams (max_values: Some(1), max_size: Some(167), added: 662, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking PeriodEnd (r:0 w:1) + /// Proof: DappStaking PeriodEnd (max_values: None, max_size: Some(48), added: 2523, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + /// Storage: DappStaking NextTierConfig (r:0 w:1) + /// Proof: DappStaking NextTierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_voting() -> Weight { + // Proof Size summary in bytes: + // Measured: `685` + // Estimated: `3870` + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(37_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: DappStaking CurrentEraInfo (r:1 w:1) + /// Proof: DappStaking CurrentEraInfo (max_values: Some(1), max_size: Some(112), added: 607, mode: MaxEncodedLen) + /// Storage: DappStaking EraRewards (r:1 w:1) + /// Proof: DappStaking EraRewards (max_values: None, max_size: Some(405), added: 2880, mode: MaxEncodedLen) + /// Storage: DappStaking DAppTiers (r:0 w:1) + /// Proof: DappStaking DAppTiers (max_values: None, max_size: Some(483), added: 2958, mode: MaxEncodedLen) + fn on_initialize_build_and_earn_to_build_and_earn() -> Weight { + // Proof Size summary in bytes: + // Measured: `73` + // Estimated: `3870` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(18_000_000, 3870) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: DappStaking ContractStake (r:101 w:0) + /// Proof: DappStaking ContractStake (max_values: Some(65535), max_size: Some(93), added: 2073, mode: MaxEncodedLen) + /// Storage: DappStaking TierConfig (r:1 w:0) + /// Proof: DappStaking TierConfig (max_values: Some(1), max_size: Some(161), added: 656, mode: MaxEncodedLen) + /// The range of component `x` is `[0, 100]`. + fn dapp_tier_assignment(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `158 + x * (33 ±0)` + // Estimated: `3063 + x * (2073 ±0)` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(12_709_998, 3063) + // Standard Error: 8_047 + .saturating_add(Weight::from_parts(2_731_946, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2073).saturating_mul(x.into())) + } +} diff --git a/pallets/inflation/src/lib.rs b/pallets/inflation/src/lib.rs index e19841c71d..717aa764f1 100644 --- a/pallets/inflation/src/lib.rs +++ b/pallets/inflation/src/lib.rs @@ -16,13 +16,92 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -//! TODO +//! # Inflation Handler Pallet +//! +//! ## Overview +//! +//! This pallet's main responsibility is handling inflation calculation & distribution. +//! +//! Inflation configuration is calculated periodically, according to the inflation parameters. +//! Based on this configuration, rewards are paid out - either per block or on demand. +//! +//! ## Cycles, Periods, Eras +//! +//! At the start of each cycle, the inflation configuration is recalculated. +//! +//! Cycle can be considered as a 'year' in the Astar network. +//! When cycle starts, inflation is calculated according to the total issuance at that point in time. +//! E.g. if 'yearly' inflation is set to be 7%, and total issuance is 200 ASTR, then the max inflation for that cycle will be 14 ASTR. +//! +//! Each cycle consists of one or more `periods`. +//! Periods are integral part of dApp staking protocol, allowing dApps to promotove themselves, attract stakers and earn rewards. +//! At the end of each period, all stakes are reset, and dApps need to repeat the process. +//! +//! Each period consists of two subperiods: `Voting` and `Build&Earn`. +//! Length of these subperiods is expressed in eras. An `era` is the core _time unit_ in dApp staking protocol. +//! When an era ends, in `Build&Earn` subperiod, rewards for dApps are calculated & assigned. +//! +//! Era's length is expressed in blocks. E.g. an era can last for 7200 blocks, which is approximately 1 day for 12 second block time. +//! +//! `Build&Earn` subperiod length is expressed in eras. E.g. if `Build&Earn` subperiod lasts for 5 eras, it means that during that subperiod, +//! dApp rewards will be calculated & assigned 5 times in total. Also, 5 distinct eras will change during that subperiod. If e.g. `Build&Earn` started at era 100, +//! with 5 eras per `Build&Earn` subperiod, then the subperiod will end at era 105. +//! +//! `Voting` subperiod always comes before `Build&Earn` subperiod. Its length is also expressed in eras, although it has to be interpreted a bit differently. +//! Even though `Voting` can last for more than 1 era in respect of length, it always takes exactly 1 era. +//! What this means is that if `Voting` lasts for 3 eras, and each era lasts 7200 blocks, then `Voting` will last for 21600 blocks. +//! But unlike `Build&Earn` subperiod, `Voting` will only take up one 'numerical' era. So if `Voting` starts at era 110, it will end at era 11. +//! +//! #### Example +//! * Cycle length: 4 periods +//! * `Voting` length: 10 eras +//! * `Build&Earn` length: 81 eras +//! * Era length: 7200 blocks +//! +//! This would mean that cycle lasts for roughly 364 days (4 * (10 + 81)). +//! +//! ## Recalculation +//! +//! When new cycle begins, inflation configuration is recalculated according to the inflation parameters & total issuance at that point in time. +//! Based on the max inflation rate, rewards for different network actors are calculated. +//! +//! Some rewards are calculated to be paid out per block, while some are per era or per period. +//! +//! ## Rewards +//! +//! ### Staker & Treasury Rewards +//! +//! These are paid out at the begininng of each block & are fixed amounts. +//! +//! ### Staker Rewards +//! +//! Staker rewards are paid out per staker, _on-demand_. +//! However, reward pool for an era is calculated at the end of each era. +//! +//! `era_reward_pool = base_staker_reward_pool_per_era + adjustable_staker_reward_pool_per_era` +//! +//! While the base staker reward pool is fixed, the adjustable part is calculated according to the total value staked & the ideal staking rate. +//! +//! ### dApp Rewards +//! +//! dApp rewards are paid out per dApp, _on-demand_. The reward is decided by the dApp staking protocol, or the tier system to be more precise. +//! This pallet only provides the total reward pool for all dApps per era. +//! +//! # Interface +//! +//! ## StakingRewardHandler +//! +//! This pallet implements `StakingRewardHandler` trait, which is used by the dApp staking protocol to get reward pools & distribute rewards. +//! #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; -use astar_primitives::{Balance, BlockNumber}; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + Balance, BlockNumber, +}; use frame_support::{pallet_prelude::*, traits::Currency}; use frame_system::{ensure_root, pallet_prelude::*}; use sp_runtime::{traits::CheckedAdd, Perquintill}; @@ -510,66 +589,3 @@ pub trait PayoutPerBlock { /// Payout reward to the collator responsible for producing the block. fn collators(reward: Imbalance); } - -// TODO: This should be moved to primitives. -// TODO2: However this ends up looking in the end, we should not duplicate these parameters in the runtime. -// Both the dApp staking & inflation pallet should use the same source. -pub trait CycleConfiguration { - /// How many different periods are there in a cycle (a 'year'). - /// - /// This value has to be at least 1. - fn periods_per_cycle() -> u32; - - /// For how many standard era lengths does the voting subperiod last. - /// - /// This value has to be at least 1. - fn eras_per_voting_subperiod() -> u32; - - /// How many standard eras are there in the build&earn subperiod. - /// - /// This value has to be at least 1. - fn eras_per_build_and_earn_subperiod() -> u32; - - /// How many blocks are there per standard era. - /// - /// This value has to be at least 1. - fn blocks_per_era() -> u32; - - /// For how many standard era lengths does the period last. - fn eras_per_period() -> u32 { - Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod()) - } - - /// For how many standard era lengths does the cylce (a 'year') last. - fn eras_per_cycle() -> u32 { - Self::eras_per_period().saturating_mul(Self::periods_per_cycle()) - } - - /// How many blocks are there per cycle (a 'year'). - fn blocks_per_cycle() -> u32 { - Self::blocks_per_era().saturating_mul(Self::eras_per_cycle()) - } - - /// For how many standard era lengths do all the build&earn subperiods in a cycle last. - fn build_and_earn_eras_per_cycle() -> u32 { - Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle()) - } -} - -// TODO: This should be moved to primitives. -/// Interface for staking reward handler. -/// -/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool. -/// Also provides a safe function for paying out rewards. -pub trait StakingRewardHandler { - /// Returns the staker reward pool & dApp reward pool for an era. - /// - /// The total staker reward pool is dynamic and depends on the total value staked. - fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance); - - /// Returns the bonus reward pool for a period. - fn bonus_reward_pool() -> Balance; - - /// Attempts to pay out the rewards to the beneficiary. - fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; -} diff --git a/pallets/inflation/src/mock.rs b/pallets/inflation/src/mock.rs index a2c0af4b6a..3cac1bca84 100644 --- a/pallets/inflation/src/mock.rs +++ b/pallets/inflation/src/mock.rs @@ -32,13 +32,12 @@ use frame_support::{ use sp_core::H256; use sp_runtime::{ - generic::Header, // TODO: create testing primitives & move it there? traits::{AccountIdConversion, BlakeTwo256, IdentityLookup}, Perquintill, }; -use astar_primitives::{Balance, BlockNumber}; -pub(crate) type AccountId = u64; // TODO: might also be nice to have this under testing primitives? +use astar_primitives::{testing::Header, Balance, BlockNumber}; +pub(crate) type AccountId = u64; /// Initial inflation params set by the mock. pub const INIT_PARAMS: InflationParameters = InflationParameters { @@ -86,7 +85,7 @@ impl frame_system::Config for Test { type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; - type Header = Header; + type Header = Header; type RuntimeEvent = RuntimeEvent; type BlockHashCount = BlockHashCount; type DbWeight = (); diff --git a/primitives/src/dapp_staking.rs b/primitives/src/dapp_staking.rs new file mode 100644 index 0000000000..92ead96b15 --- /dev/null +++ b/primitives/src/dapp_staking.rs @@ -0,0 +1,85 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::{Balance, BlockNumber}; + +/// Configuration for cycles, periods, subperiods & eras. +/// +/// * `cycle` - Time unit similar to 'year' in the real world. Consists of one or more periods. At the beginning of each cycle, inflation is recalculated. +/// * `period` - Period consists of two distinct subperiods: `Voting` & `Build&Earn`. They are integral parts of dApp staking. +/// Length is expressed in standard eras or just _eras_. +/// * `era` - Era is the basic time unit in the dApp staking protocol. At the end of each era, reward pools for stakers & dApps are calculated. +/// Era length is expressed in blocks. +pub trait CycleConfiguration { + /// How many different periods are there in a cycle (a 'year'). + /// + /// This value has to be at least 1. + fn periods_per_cycle() -> u32; + + /// For how many standard era lengths does the voting subperiod last. + /// + /// This value has to be at least 1. + fn eras_per_voting_subperiod() -> u32; + + /// How many standard eras are there in the build&earn subperiod. + /// + /// This value has to be at least 1. + fn eras_per_build_and_earn_subperiod() -> u32; + + /// How many blocks are there per standard era. + /// + /// This value has to be at least 1. + fn blocks_per_era() -> BlockNumber; + + /// For how many standard era lengths does the period last. + fn eras_per_period() -> u32 { + Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod()) + } + + /// For how many standard era lengths does the cylce (a 'year') last. + fn eras_per_cycle() -> u32 { + Self::eras_per_period().saturating_mul(Self::periods_per_cycle()) + } + + /// How many blocks are there per cycle (a 'year'). + fn blocks_per_cycle() -> BlockNumber { + Self::blocks_per_era().saturating_mul(Self::eras_per_cycle()) + } + + /// For how many standard era lengths do all the build&earn subperiods in a cycle last. + fn build_and_earn_eras_per_cycle() -> u32 { + Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle()) + } +} + +/// Interface for staking reward handler. +/// +/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool. +/// Also provides a safe function for paying out rewards. +pub trait StakingRewardHandler { + /// Returns the staker reward pool & dApp reward pool for an era. + /// + /// The total staker reward pool is dynamic and depends on the total value staked. + fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance); + + /// Returns the bonus reward pool for a period. + fn bonus_reward_pool() -> Balance; + + /// Attempts to pay out the rewards to the beneficiary. + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 010feba290..a531b87334 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -35,6 +35,12 @@ pub mod evm; /// Precompiles pub mod precompiles; +/// dApp staking & inflation primitives. +pub mod dapp_staking; + +/// Useful primitives for testing. +pub mod testing; + /// Benchmark primitives #[cfg(feature = "runtime-benchmarks")] pub mod benchmarks; diff --git a/primitives/src/testing.rs b/primitives/src/testing.rs new file mode 100644 index 0000000000..aa800582f1 --- /dev/null +++ b/primitives/src/testing.rs @@ -0,0 +1,24 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::BlockNumber; + +use sp_runtime::{generic::Header as GenericHeader, traits::BlakeTwo256}; + +/// Test `Header` type aligned with Astar primitives. +pub type Header = GenericHeader; diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index e36a348952..f5e318d898 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -24,12 +24,12 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); -use astar_primitives::evm::HashedDefaultMappings; use frame_support::{ construct_runtime, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Currency, EitherOfDiverse, - EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, Nothing, OnFinalize, WithdrawReasons, + fungible::Unbalanced as FunUnbalanced, AsEnsureOriginWithArg, ConstU128, ConstU32, + ConstU64, Currency, EitherOfDiverse, EqualPrivilegeOnly, FindAuthor, Get, InstanceFilter, + Nothing, OnFinalize, WithdrawReasons, }, weights::{ constants::{ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -63,8 +63,9 @@ use sp_runtime::{ use sp_std::prelude::*; pub use astar_primitives::{ - evm::EvmRevertCodeHandler, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, - Index, Signature, + dapp_staking::{CycleConfiguration, StakingRewardHandler}, + evm::{EvmRevertCodeHandler, HashedDefaultMappings}, + AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; pub use pallet_block_rewards_hybrid::RewardDistributionConfig; @@ -296,9 +297,9 @@ impl pallet_balances::Config for Runtime { type AccountStore = System; type WeightInfo = weights::pallet_balances::SubstrateWeight; type HoldIdentifier = (); - type FreezeIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxFreezes = ConstU32<1>; } parameter_types! { @@ -511,46 +512,31 @@ impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { } } -pub struct DummyRewardPoolProvider; -impl pallet_dapp_staking_v3::RewardPoolProvider for DummyRewardPoolProvider { - fn normal_reward_pools() -> (Balance, Balance) { - ( - Balance::from(1_000_000_000_000 * AST), - Balance::from(1_000_000_000 * AST), - ) - } - fn bonus_reward_pool() -> Balance { - Balance::from(3_000_000 * AST) - } -} - #[cfg(feature = "runtime-benchmarks")] -pub struct BenchmarkHelper(sp_std::marker::PhantomData); +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); #[cfg(feature = "runtime-benchmarks")] -impl pallet_dapp_staking_v3::BenchmarkHelper> - for BenchmarkHelper> +impl pallet_dapp_staking_v3::BenchmarkHelper, AccountId> + for BenchmarkHelper, AccountId> { fn get_smart_contract(id: u32) -> SmartContract { SmartContract::Wasm(AccountId::from([id as u8; 32])) } -} -parameter_types! { - pub const StandardEraLength: BlockNumber = 30; // should be 1 minute per standard era - pub const StandardErasPerVotingSubperiod: u32 = 2; - pub const StandardErasPerBuildAndEarnSubperiod: u32 = 10; + fn set_balance(account: &AccountId, amount: Balance) { + Balances::write_balance(account, amount) + .expect("Must succeed in test/benchmark environment."); + } } impl pallet_dapp_staking_v3::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; type Currency = Balances; type SmartContract = SmartContract; type ManagerOrigin = frame_system::EnsureRoot; type NativePriceProvider = DummyPriceProvider; - type RewardPoolProvider = DummyRewardPoolProvider; - type StandardEraLength = StandardEraLength; - type StandardErasPerVotingSubperiod = StandardErasPerVotingSubperiod; - type StandardErasPerBuildAndEarnSubperiod = StandardErasPerBuildAndEarnSubperiod; + type StakingRewardHandler = Inflation; + type CycleConfiguration = InflationCycleConfig; type EraRewardSpanLength = ConstU32<8>; type RewardRetentionInPeriods = ConstU32<2>; type MaxNumberOfContracts = ConstU32<100>; @@ -560,8 +546,9 @@ impl pallet_dapp_staking_v3::Config for Runtime { type MaxNumberOfStakedContracts = ConstU32<3>; type MinimumStakeAmount = ConstU128; type NumberOfTiers = ConstU32<4>; + type WeightInfo = pallet_dapp_staking_v3::weights::SubstrateWeight; #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = BenchmarkHelper>; + type BenchmarkHelper = BenchmarkHelper, AccountId>; } pub struct InflationPayoutPerBlock; @@ -576,21 +563,21 @@ impl pallet_inflation::PayoutPerBlock for InflationPayoutPerB } pub struct InflationCycleConfig; -impl pallet_inflation::CycleConfiguration for InflationCycleConfig { +impl CycleConfiguration for InflationCycleConfig { fn periods_per_cycle() -> u32 { 4 } fn eras_per_voting_subperiod() -> u32 { - StandardErasPerVotingSubperiod::get() + 2 } fn eras_per_build_and_earn_subperiod() -> u32 { - StandardErasPerBuildAndEarnSubperiod::get() + 22 } - fn blocks_per_era() -> u32 { - StandardEraLength::get() + fn blocks_per_era() -> BlockNumber { + 30 } }