Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stakingv3: rank dapps in-between tiers #1240

Merged
merged 24 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, TierId};
use astar_primitives::dapp_staking::{DAppId, EraNumber, PeriodNumber, RankedTier};
use astar_primitives::BlockNumber;
pub use sp_std::collections::btree_map::BTreeMap;

Expand All @@ -42,6 +42,6 @@ sp_api::decl_runtime_apis! {
fn blocks_per_era() -> BlockNumber;

/// Get dApp tier assignment for the given dApp.
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId>;
fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier>;
Dinonard marked this conversation as resolved.
Show resolved Hide resolved
}
}
9 changes: 6 additions & 3 deletions pallets/dapp-staking-v3/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ mod benchmarks {

#[block]
{
let (dapp_tiers, _) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
let (dapp_tiers, _count) = Pallet::<T>::get_dapp_tier_assignment_and_rewards(
reward_era,
reward_period,
reward_pool,
Expand Down Expand Up @@ -1041,14 +1041,17 @@ mod benchmarks {
&cleanup_marker.dapp_tiers_index,
DAppTierRewardsFor::<T> {
dapps: (0..T::MaxNumberOfContracts::get())
.map(|dapp_id| (dapp_id as DAppId, 0))
.collect::<BTreeMap<DAppId, TierId>>()
.map(|dapp_id| (dapp_id as DAppId, RankedTier::new_saturated(0, 0)))
.collect::<BTreeMap<DAppId, RankedTier>>()
.try_into()
.expect("Using `MaxNumberOfContracts` as length; QED."),
rewards: vec![1_000_000_000_000; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
period: 1,
rank_rewards: vec![0; T::NumberOfTiers::get() as usize]
.try_into()
.expect("Using `NumberOfTiers` as length; QED."),
},
);

Expand Down
122 changes: 81 additions & 41 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub use sp_std::vec::Vec;
use astar_primitives::{
dapp_staking::{
AccountCheck, CycleConfiguration, DAppId, EraNumber, Observer as DAppStakingObserver,
PeriodNumber, SmartContractHandle, StakingRewardHandler, TierId, TierSlots as TierSlotFunc,
PeriodNumber, RankedTier, SmartContractHandle, StakingRewardHandler, TierId,
TierSlots as TierSlotFunc,
},
oracle::PriceProvider,
Balance, BlockNumber,
Expand All @@ -71,7 +72,9 @@ mod benchmarking;
mod types;
pub use types::*;

pub mod migration;
pub mod weights;

pub use weights::WeightInfo;

const LOG_TARGET: &str = "dapp-staking";
Expand All @@ -91,7 +94,7 @@ pub mod pallet {
use super::*;

/// The current storage version.
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(6);
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(7);

#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
Expand Down Expand Up @@ -198,6 +201,10 @@ pub mod pallet {
#[pallet::constant]
type NumberOfTiers: Get<u32>;

/// Tier ranking enabled.
#[pallet::constant]
type RankingEnabled: Get<bool>;
Dinonard marked this conversation as resolved.
Show resolved Hide resolved

/// Weight info for various calls & operations in the pallet.
type WeightInfo: WeightInfo;

Expand Down Expand Up @@ -1403,14 +1410,16 @@ pub mod pallet {
Error::<T>::RewardExpired
);

let (amount, tier_id) =
let (amount, ranked_tier) =
dapp_tiers
.try_claim(dapp_info.id)
.map_err(|error| match error {
DAppTierError::NoDAppInTiers => Error::<T>::NoClaimableRewards,
_ => Error::<T>::InternalClaimDAppError,
})?;

let (tier_id, _rank) = ranked_tier.deconstruct();

// Get reward destination, and deposit the reward.
let beneficiary = dapp_info.reward_beneficiary();
T::StakingRewardHandler::payout_reward(&beneficiary, amount)
Expand Down Expand Up @@ -1670,7 +1679,7 @@ pub mod pallet {
}

/// Returns the dApp tier assignment for the current era, based on the current stake amounts.
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, TierId> {
pub fn get_dapp_tier_assignment() -> BTreeMap<DAppId, RankedTier> {
let protocol_state = ActiveProtocolState::<T>::get();

let (dapp_tiers, _count) = Self::get_dapp_tier_assignment_and_rewards(
Expand All @@ -1691,7 +1700,11 @@ pub mod pallet {
///
/// 2. Sort the entries by the score, in descending order - the top score dApp comes first.
///
/// 3. Read in tier configuration. This contains information about how many slots per tier there are,
/// 3. 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.
///
/// 4. Read in tier configuration. This contains information about how many slots per tier there are,
/// 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.
///
Expand All @@ -1705,10 +1718,6 @@ pub mod pallet {
/// ```
/// (Sort the entries by dApp ID, in ascending order. This is so we can efficiently search for them using binary search.)
///
/// 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_and_rewards(
Expand Down Expand Up @@ -1737,47 +1746,16 @@ pub mod pallet {
// Sort by amount staked, in reverse - top dApp will end in the first place, 0th index.
dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1));

// 3.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let tier_config = TierConfig::<T>::get();

let mut global_idx = 0;
let mut tier_id = 0;
for (tier_capacity, tier_threshold) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
{
let max_idx = global_idx
.saturating_add(*tier_capacity as usize)
.min(dapp_stakes.len());

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() {
if tier_threshold.is_satisfied(*stake_amount) {
global_idx.saturating_inc();
dapp_tiers.insert(*dapp_id, tier_id);
} else {
break;
}
}

tier_id.saturating_inc();
}

// 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.
//
// In the current implementation, the dApp with the lower dApp Id has the advantage.
// There is no guarantee this will persist in the future, so it's best for dApps to do their
// best to avoid getting themselves into such situations.

// 4. Calculate rewards.
// 3. Calculate rewards.
let tier_rewards = tier_config
.reward_portion
.iter()
Expand All @@ -1791,6 +1769,67 @@ pub mod pallet {
})
.collect::<Vec<_>>();

// 4.
// Iterate over configured tier and potential dApps.
// Each dApp will be assigned to the best possible tier if it satisfies the required condition,
// and tier capacity hasn't been filled yet.
let mut dapp_tiers = BTreeMap::new();
let mut tier_slots = BTreeMap::new();

let mut upper_bound = Balance::zero();
let mut rank_rewards = Vec::new();

for (tier_id, (tier_capacity, tier_threshold)) in tier_config
.slots_per_tier
.iter()
.zip(tier_config.tier_thresholds.iter())
.enumerate()
{
let lower_bound = tier_threshold.threshold();

// Iterate over dApps until one of two conditions has been met:
// 1. Tier has no more capacity
// 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either)
for (dapp_id, staked_amount) in dapp_stakes
.iter()
.skip(dapp_tiers.len())
.take_while(|(_, amount)| tier_threshold.is_satisfied(*amount))
.take(*tier_capacity as usize)
ermalkaleci marked this conversation as resolved.
Show resolved Hide resolved
{
let rank = if T::RankingEnabled::get() {
RankedTier::find_rank(lower_bound, upper_bound, *staked_amount)
} else {
0
};
tier_slots.insert(*dapp_id, RankedTier::new_saturated(tier_id as u8, rank));
}

// sum of all ranks for this tier
let ranks_sum = tier_slots
.iter()
.fold(0u32, |accum, (_, x)| accum.saturating_add(x.rank().into()));
Dinonard marked this conversation as resolved.
Show resolved Hide resolved

let reward_per_rank = if ranks_sum.is_zero() {
Balance::zero()
} else {
// calculate reward per rank
let tier_reward = tier_rewards.get(tier_id).copied().unwrap_or_default();
let empty_slots = tier_capacity.saturating_sub(tier_slots.len() as u16);
let remaining_reward = tier_reward.saturating_mul(empty_slots.into());
// make sure required reward doesn't exceed remaining reward
let reward_per_rank = tier_reward.saturating_div(RankedTier::MAX_RANK.into());
let expected_reward_for_ranks =
reward_per_rank.saturating_mul(ranks_sum.into());
let reward_for_ranks = expected_reward_for_ranks.min(remaining_reward);
// re-calculate reward per rank based on available reward
reward_for_ranks.saturating_div(ranks_sum.into())
};

rank_rewards.push(reward_per_rank);
dapp_tiers.append(&mut tier_slots);
upper_bound = lower_bound; // current threshold becomes upper bound for next tier
}

// 5.
// Prepare and return tier & rewards info.
// In case rewards creation fails, we just write the default value. This should never happen though.
Expand All @@ -1799,6 +1838,7 @@ pub mod pallet {
dapp_tiers,
tier_rewards,
period,
rank_rewards,
)
.unwrap_or_default(),
counter,
Expand Down
112 changes: 112 additions & 0 deletions pallets/dapp-staking-v3/src/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// This file is part of Astar.

// Copyright (C) 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 <http://www.gnu.org/licenses/>.

use super::*;
use frame_support::traits::OnRuntimeUpgrade;

#[cfg(feature = "try-runtime")]
use sp_std::vec::Vec;

#[cfg(feature = "try-runtime")]
use sp_runtime::TryRuntimeError;

/// Exports for versioned migration `type`s for this pallet.
pub mod versioned_migrations {
use super::*;

/// Migration V6 to V7 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
/// the migration is only performed when on-chain version is 6.
pub type V6ToV7<T> = frame_support::migrations::VersionedMigration<
6,
7,
v7::VersionMigrateV6ToV7<T>,
Pallet<T>,
<T as frame_system::Config>::DbWeight,
>;
}

/// Translate DAppTiers to include rank rewards.
mod v7 {
use super::*;
use crate::migration::v6::DAppTierRewards as DAppTierRewardsV6;

pub struct VersionMigrateV6ToV7<T>(PhantomData<T>);

impl<T: Config> OnRuntimeUpgrade for VersionMigrateV6ToV7<T> {
fn on_runtime_upgrade() -> Weight {
let current = Pallet::<T>::current_storage_version();

let mut translated = 0usize;
DAppTiers::<T>::translate::<
DAppTierRewardsV6<T::MaxNumberOfContracts, T::NumberOfTiers>,
_,
>(|_key, old_value| {
translated.saturating_inc();
Some(DAppTierRewards {
dapps: old_value.dapps,
rewards: old_value.rewards,
period: old_value.period,
rank_rewards: Default::default(),
ermalkaleci marked this conversation as resolved.
Show resolved Hide resolved
})
});

current.put::<Pallet<T>>();

log::info!("Upgraded {translated} dAppTiers to {current:?}");

T::DbWeight::get().reads_writes(1 + translated as u64, 1 + translated as u64)
}

#[cfg(feature = "try-runtime")]
fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
Ok(Vec::new())
}

#[cfg(feature = "try-runtime")]
fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
ensure!(
Pallet::<T>::on_chain_storage_version() >= 7,
"dapp-staking-v3::migration::v7: wrong storage version"
);
Ok(())
}
}
}

pub mod v6 {
use astar_primitives::{
dapp_staking::{DAppId, PeriodNumber, RankedTier},
Balance,
};
use frame_support::{
pallet_prelude::{Decode, Get},
BoundedBTreeMap, BoundedVec,
};

/// Information about all of the dApps that got into tiers, and tier rewards
#[derive(Decode)]
pub struct DAppTierRewards<MD: Get<u32>, NT: Get<u32>> {
/// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime)
pub dapps: BoundedBTreeMap<DAppId, RankedTier, MD>,
/// Rewards for each tier. First entry refers to the first tier, and so on.
pub rewards: BoundedVec<Balance, NT>,
/// Period during which this struct was created.
#[codec(compact)]
pub period: PeriodNumber,
}
}
3 changes: 2 additions & 1 deletion pallets/dapp-staking-v3/src/test/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{

use frame_support::{
construct_runtime, parameter_types,
traits::{fungible::Mutate as FunMutate, ConstU128, ConstU32},
traits::{fungible::Mutate as FunMutate, ConstBool, ConstU128, ConstU32},
weights::Weight,
};
use sp_arithmetic::fixed_point::FixedU128;
Expand Down Expand Up @@ -221,6 +221,7 @@ impl pallet_dapp_staking::Config for Test {
type MaxNumberOfStakedContracts = ConstU32<5>;
type MinimumStakeAmount = ConstU128<3>;
type NumberOfTiers = ConstU32<4>;
type RankingEnabled = ConstBool<true>;
type WeightInfo = weights::SubstrateWeight<Test>;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper = BenchmarkHelper<MockSmartContract, AccountId>;
Expand Down
Loading
Loading