Skip to content

Commit

Permalink
Tier assignemnt
Browse files Browse the repository at this point in the history
  • Loading branch information
Dinonard committed Oct 23, 2023
1 parent 3d2d146 commit 894d10d
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 18 deletions.
91 changes: 80 additions & 11 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,17 @@ pub mod pallet {
/// Tier configuration to be used during the newly started period
#[pallet::storage]
pub type NextTierConfig<T: Config> =
StorageValue<_, TierConfiguration<T::NumberOfTiers>, ValueQuery>;
StorageValue<_, TiersConfiguration<T::NumberOfTiers>, ValueQuery>;

/// Tier configuration user for current & preceding eras.
#[pallet::storage]
pub type TierConfig<T: Config> =
StorageValue<_, TierConfiguration<T::NumberOfTiers>, ValueQuery>;
StorageValue<_, TiersConfiguration<T::NumberOfTiers>, ValueQuery>;

/// Information about which tier a dApp belonged to in a specific era.
#[pallet::storage]
pub type DAppTiers<T: Config> =
StorageMap<_, Twox64Concat, EraNumber, DAppTierRewardsFor<T>, OptionQuery>;

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
Expand Down Expand Up @@ -411,8 +416,6 @@ pub mod pallet {
)
}
PeriodType::BuildAndEarn => {
// TODO: trigger dApp tier reward calculation here. This will be implemented later.

let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?)
let dapp_reward_pool = Balance::from(1_000_000_000u128); // TODO: same as above
let era_reward = EraReward {
Expand All @@ -421,6 +424,14 @@ pub mod pallet {
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::<T>::insert(&current_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
Expand Down Expand Up @@ -1282,13 +1293,13 @@ pub mod pallet {
}

// TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks.
pub fn dapp_tier_assignment(era: EraNumber, period: PeriodNumber) {
// TODO2: documentation
pub fn get_dapp_tier_assignment(
era: EraNumber,
period: PeriodNumber,
dapp_reward_pool: Balance,
) -> DAppTierRewardsFor<T> {
let tier_config = TierConfig::<T>::get();
// TODO: this really looks ugly, and too complicated. Botom line is, this value has to exist. If it doesn't we have to assume it's `Default`.
// Rewards will just end up being all zeroes.
let reward_info = EraRewards::<T>::get(Self::era_reward_span_index(era))
.map(|span| span.get(era).map(|x| *x).unwrap_or_default())
.unwrap_or_default();

let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::<T>::count() as usize);

Expand All @@ -1307,14 +1318,72 @@ pub mod pallet {
};

// TODO: maybe also push the 'Label' here?
// TODO2: proposition for label handling:
// Split them into 'musts' and 'good-to-have'
// In case of 'must', reduce appropriate tier size, and insert them at the end
// For good to have, we can insert them immediately, and then see if we need to adjust them later.
// Anyhow, labels bring complexity. For starters, we should only deliver the one for 'bootstraping' purposes.
dapp_stakes.push((dapp_info.id, stake_amount.total()));
}

// 2.
// 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));

// TODO: continue here
// 3.
// Calculate slices representing each tier
let mut dapp_tiers = Vec::with_capacity(dapp_stakes.len());

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);

// 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)
for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() {
if tier_threshold.is_satisfied(*stake_amount) {
global_idx.saturating_accrue(1);
dapp_tiers.push(DAppTier {
dapp_id: *dapp_id,
tier_id: Some(tier_id),
});
} else {
break;
}
}

tier_id.saturating_accrue(1);
}

// 4.
// Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is guaranteed due to lack of duplicated Ids)
// TODO: Sorting requirement can change - if we put it into tree, then we don't need to sort (explicitly at least).
// Important requirement is to have efficient deletion, and fast lookup. Sorted vector with entry like (dAppId, Option<tier>) is probably the best.
// The drawback being the size of the struct DOES NOT decrease with each claim.
// But then again, this will be used for dApp reward claiming, so 'best case scenario' (or worst) is ~1000 claims per day which is still very minor.
dapp_tiers.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id));

// 5. Calculate rewards.
let tier_rewards = tier_config
.reward_portion
.iter()
.map(|percent| *percent * dapp_reward_pool)
.collect::<Vec<_>>();

// 6.
// Prepare and return tier & rewards info
// In case rewards creation fails, we just write the default value. This should never happen though.
DAppTierRewards::<MaxNumberOfContractsU32<T>, T::NumberOfTiers>::new(
dapp_tiers,
tier_rewards,
)
.unwrap_or_default()
}
}
}
2 changes: 1 addition & 1 deletion pallets/dapp-staking-v3/src/test/tests_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1559,7 +1559,7 @@ fn tier_slot_configuration_basic_tests() {
assert!(params.is_valid(), "Example params must be valid!");

// Create a configuration with some values
let init_config = TierConfiguration::<TiersNum> {
let init_config = TiersConfiguration::<TiersNum> {
number_of_slots: 100,
slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(),
reward_portion: params.reward_portion.clone(),
Expand Down
106 changes: 100 additions & 6 deletions pallets/dapp-staking-v3/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ use crate::pallet::Config;
// Convenience type for `AccountLedger` usage.
pub type AccountLedgerFor<T> = AccountLedger<BlockNumberFor<T>, <T as Config>::MaxUnlockingChunks>;

// Convenience type for `DAppTierRewards` usage.
pub type DAppTierRewardsFor<T> =
DAppTierRewards<MaxNumberOfContractsU32<T>, <T as Config>::NumberOfTiers>;

// Helper struct for converting `u16` getter into `u32`
pub struct MaxNumberOfContractsU32<T: Config>(PhantomData<T>);
impl<T: Config> Get<u32> for MaxNumberOfContractsU32<T> {
fn get() -> u32 {
T::MaxNumberOfContracts::get() as u32
}
}

/// Era number type
pub type EraNumber = u32;
/// Period number type
Expand Down Expand Up @@ -1355,6 +1367,16 @@ pub enum TierThreshold {
// Otherwise we could allow e.g. tier 3 to go below tier 4, which doesn't make sense.
}

impl TierThreshold {
/// Used to check if stake amount satisfies the threshold or not.
pub fn is_satisfied(&self, stake: Balance) -> bool {
match self {
Self::FixedTvlAmount { amount } => stake >= *amount,
Self::DynamicTvlAmount { amount } => stake >= *amount,
}
}
}

/// Top level description of tier slot parameters used to calculate tier configuration.
#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)]
#[scale_info(skip_type_params(NT))]
Expand Down Expand Up @@ -1393,10 +1415,12 @@ impl<NT: Get<u32>> Default for TierParameters<NT> {
}
}

// TODO: refactor these structs so we only have 1 bounded vector, where each entry contains all the necessary info to describe the tier

/// Configuration of dApp tiers.
#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)]
#[scale_info(skip_type_params(NT))]
pub struct TierConfiguration<NT: Get<u32>> {
pub struct TiersConfiguration<NT: Get<u32>> {
/// Total number of slots.
#[codec(compact)]
pub number_of_slots: u16,
Expand All @@ -1412,7 +1436,7 @@ pub struct TierConfiguration<NT: Get<u32>> {
pub tier_thresholds: BoundedVec<TierThreshold, NT>,
}

impl<NT: Get<u32>> Default for TierConfiguration<NT> {
impl<NT: Get<u32>> Default for TiersConfiguration<NT> {
fn default() -> Self {
Self {
number_of_slots: 0,
Expand All @@ -1423,7 +1447,7 @@ impl<NT: Get<u32>> Default for TierConfiguration<NT> {
}
}

impl<NT: Get<u32>> TierConfiguration<NT> {
impl<NT: Get<u32>> TiersConfiguration<NT> {
/// Check if parameters are valid.
pub fn is_valid(&self) -> bool {
let number_of_tiers: usize = NT::get() as usize;
Expand All @@ -1435,7 +1459,7 @@ impl<NT: Get<u32>> TierConfiguration<NT> {
&& self.slots_per_tier.iter().fold(0, |acc, x| acc + x) == self.number_of_slots
}

/// TODO
/// 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<NT>) -> Self {
let new_number_of_slots = Self::calculate_number_of_slots(native_price);

Expand All @@ -1445,13 +1469,13 @@ impl<NT: Get<u32>> TierConfiguration<NT> {
.clone()
.into_inner()
.iter()
.map(|x| *x * new_number_of_slots as u128)
.map(|percent| *percent * new_number_of_slots as u128)
.map(|x| x.unique_saturated_into())
.collect();
let new_slots_per_tier =
BoundedVec::<u16, NT>::try_from(new_slots_per_tier).unwrap_or_default();

// TODO: document this, and definitely refactor it to be simpler.
// TODO: document this!
let new_tier_thresholds = if new_number_of_slots > self.number_of_slots {
let delta_threshold_decrease = FixedU64::from_rational(
(new_number_of_slots - self.number_of_slots).into(),
Expand Down Expand Up @@ -1508,3 +1532,73 @@ impl<NT: Get<u32>> TierConfiguration<NT> {
result.unique_saturated_into()
}
}

/// Used to represent into which tier does a particular dApp fall into.
///
/// In case tier Id is `None`, it means that either reward was already claimed, or dApp is not eligible for rewards.
#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)]
pub struct DAppTier {
/// Unique dApp id in dApp staking protocol.
#[codec(compact)]
pub dapp_id: DAppId,
/// `Some(tier_id)` if dApp belongs to tier and has unclaimed rewards, `None` otherwise.
pub tier_id: Option<u8>,
}

/// Information about all of the dApps that got into tiers, and tier rewards
#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)]
#[scale_info(skip_type_params(MD, NT))]
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: BoundedVec<DAppTier, MD>,
/// Rewards for each tier. First entry refers to the first tier, and so on.
pub rewards: BoundedVec<Balance, NT>,
}

impl<MD: Get<u32>, NT: Get<u32>> Default for DAppTierRewards<MD, NT> {
fn default() -> Self {
Self {
dapps: BoundedVec::default(),
rewards: BoundedVec::default(),
}
}
}

impl<MD: Get<u32>, NT: Get<u32>> DAppTierRewards<MD, NT> {
/// Attempt to construct `DAppTierRewards` struct.
/// If the provided arguments exceed the allowed capacity, return an error.
pub fn new(dapps: Vec<DAppTier>, rewards: Vec<Balance>) -> Result<Self, ()> {
let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?;
let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?;
Ok(Self { dapps, rewards })
}

/// Consume reward for the specified dapp id, returning its amount.
/// In case dapp isn't applicable for rewards, or they have already been consumed, returns **zero**.
pub fn consume(&mut self, dapp_id: DAppId) -> Balance {
// Check if dApp Id exists.
let dapp_idx = match self
.dapps
.binary_search_by(|entry| entry.dapp_id.cmp(&dapp_id))
{
Ok(idx) => idx,
// dApp Id doesn't exist
_ => return Balance::zero(),
};

match self.dapps.get_mut(dapp_idx) {
Some(dapp_tier) => {
if let Some(tier_id) = dapp_tier.tier_id.take() {
self.rewards
.get(tier_id as usize)
.map_or(Balance::zero(), |x| *x)
} else {
// In case reward has already been claimed
Balance::zero()
}
}
// unreachable code, at this point it was proved that index exists
_ => Balance::zero(),
}
}
}

0 comments on commit 894d10d

Please sign in to comment.