From abf7b57ccb16fc9dcea0b524f97bd9de00cf8991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 27 Oct 2022 10:51:09 +0100 Subject: [PATCH] Bugfix/correct staking supply accounting (#1706) * Added staking_supply_scale_factor field on RewardingParams * Scaling the amount of tokens released to the circulating/staking supplies --- Cargo.lock | 2 + .../contracts-common/Cargo.toml | 2 + .../contracts-common/src/types.rs | 123 +++++++++++++++++- .../mixnet-contract/src/msg.rs | 2 + .../mixnet-contract/src/reward_params.rs | 5 + .../src/rewarding/simulator/mod.rs | 10 +- .../mixnet-contract/src/types.rs | 81 ------------ contracts/Cargo.lock | 2 + contracts/mixnet/src/contract.rs | 2 + contracts/mixnet/src/rewards/helpers.rs | 3 +- contracts/mixnet/src/support/tests/mod.rs | 1 + nym-wallet/Cargo.lock | 2 + 12 files changed, 151 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1847572677..85cfc403be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,7 +737,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 2145acc513..22fb97bc68 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -9,3 +9,5 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 6c25d95552..e9b399c02e 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -1,7 +1,128 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use serde::{Deserialize, Serialize}; +use cosmwasm_std::Decimal; +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt::{self, Display, Formatter}; +use std::ops::Mul; +use std::str::FromStr; +use thiserror::Error; + +pub fn truncate_decimal(amount: Decimal) -> Uint128 { + amount * Uint128::new(1) +} + +#[derive(Error, Debug)] +pub enum ContractsCommonError { + #[error("Provided percent value ({0}) is greater than 100%")] + InvalidPercent(Decimal), + + #[error("{source}")] + StdErr { + #[from] + source: cosmwasm_std::StdError, + }, +} + +/// Percent represents a value between 0 and 100% +/// (i.e. between 0.0 and 1.0) +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); + +impl Percent { + pub fn new(value: Decimal) -> Result { + if value > Decimal::one() { + Err(ContractsCommonError::InvalidPercent(value)) + } else { + Ok(Percent(value)) + } + } + + pub fn is_zero(&self) -> bool { + self.0 == Decimal::zero() + } + + pub fn zero() -> Self { + Self(Decimal::zero()) + } + + pub fn hundred() -> Self { + Self(Decimal::one()) + } + + pub fn from_percentage_value(value: u64) -> Result { + Percent::new(Decimal::percent(value)) + } + + pub fn value(&self) -> Decimal { + self.0 + } + + pub fn round_to_integer(&self) -> u8 { + let hundred = Decimal::from_ratio(100u32, 1u32); + // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range + truncate_decimal(hundred * self.0).u128() as u8 + } +} + +impl Display for Percent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; + write!(f, "{}%", adjusted) + } +} + +impl FromStr for Percent { + type Err = ContractsCommonError; + + fn from_str(s: &str) -> Result { + Percent::new(Decimal::from_str(s)?) + } +} + +impl Mul for Percent { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Decimal { + type Output = Decimal; + + fn mul(self, rhs: Percent) -> Self::Output { + rhs * self + } +} + +impl Mul for Percent { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + self.0 * rhs + } +} + +// implement custom Deserialize because we want to validate Percent has the correct range +fn de_decimal_percent<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Decimal::deserialize(deserializer)?; + if v > Decimal::one() { + Err(D::Error::custom( + "provided decimal percent is larger than 100%", + )) + } else { + Ok(v) + } +} // TODO: there's no reason this couldn't be used for proper binaries, but in that case // perhaps the struct should get renamed and moved to a "more" common crate diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 5c4572aee4..c01b47cf02 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -32,6 +32,7 @@ pub struct InitialRewardingParams { pub initial_reward_pool: Decimal, pub initial_staking_supply: Decimal, + pub staking_supply_scale_factor: Percent, pub sybil_resistance: Percent, pub active_set_work_factor: Decimal, pub interval_pool_emission: Percent, @@ -52,6 +53,7 @@ impl InitialRewardingParams { interval: IntervalRewardParams { reward_pool: self.initial_reward_pool, staking_supply: self.initial_staking_supply, + staking_supply_scale_factor: self.staking_supply_scale_factor, epoch_reward_budget, stake_saturation_point, sybil_resistance: self.sybil_resistance, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index fc543a3c02..73459f42d9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -26,6 +26,11 @@ pub struct IntervalRewardParams { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub staking_supply: Decimal, + /// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set. + /// Also known as `beta`. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub staking_supply_scale_factor: Percent, + // computed values /// Current value of the computed reward budget per epoch, per node. /// It is expected to be constant throughout the interval. diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index bd4fc8d8d3..5fe1c46175 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -40,7 +40,12 @@ impl Simulator { if self.interval.current_interval_id() + 1 == updated.current_interval_id() { let old = self.system_rewarding_params.interval; let reward_pool = old.reward_pool - self.pending_reward_pool_emission; - let staking_supply = old.staking_supply + self.pending_reward_pool_emission; + let staking_supply = old.staking_supply + + self + .system_rewarding_params + .interval + .staking_supply_scale_factor + * self.pending_reward_pool_emission; let epoch_reward_budget = reward_pool / Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap() * old.interval_pool_emission.value(); @@ -51,6 +56,7 @@ impl Simulator { interval: IntervalRewardParams { reward_pool, staking_supply, + staking_supply_scale_factor: old.staking_supply_scale_factor, epoch_reward_budget, stake_saturation_point, sybil_resistance: old.sybil_resistance, @@ -209,6 +215,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget, stake_saturation_point, sybil_resistance: Percent::from_percentage_value(30).unwrap(), @@ -537,6 +544,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget, stake_saturation_point, sybil_resistance: Percent::from_percentage_value(30).unwrap(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 7d6e8f2d0d..b07929578a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -24,87 +24,6 @@ pub type BlockHeight = u64; pub type EpochEventId = u32; pub type IntervalEventId = u32; -/// Percent represents a value between 0 and 100% -/// (i.e. between 0.0 and 1.0) -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, -)] -pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); - -impl Percent { - pub fn new(value: Decimal) -> Result { - if value > Decimal::one() { - Err(MixnetContractError::InvalidPercent) - } else { - Ok(Percent(value)) - } - } - - pub fn is_zero(&self) -> bool { - self.0 == Decimal::zero() - } - - pub fn from_percentage_value(value: u64) -> Result { - Percent::new(Decimal::percent(value)) - } - - pub fn value(&self) -> Decimal { - self.0 - } - - pub fn round_to_integer(&self) -> u8 { - let hundred = Decimal::from_ratio(100u32, 1u32); - // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range - truncate_decimal(hundred * self.0).u128() as u8 - } -} - -impl Display for Percent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; - write!(f, "{}%", adjusted) - } -} - -impl Mul for Percent { - type Output = Decimal; - - fn mul(self, rhs: Decimal) -> Self::Output { - self.0 * rhs - } -} - -impl Mul for Decimal { - type Output = Decimal; - - fn mul(self, rhs: Percent) -> Self::Output { - rhs * self - } -} - -impl Mul for Percent { - type Output = Uint128; - - fn mul(self, rhs: Uint128) -> Self::Output { - self.0 * rhs - } -} - -// implement custom Deserialize because we want to validate Percent has the correct range -fn de_decimal_percent<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let v = Decimal::deserialize(deserializer)?; - if v > Decimal::one() { - Err(D::Error::custom( - "provided decimal percent is larger than 100%", - )) - } else { - Ok(v) - } -} - #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { pub layer1: u64, diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3490bbf2fd..9601623a33 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -221,7 +221,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index be9c849425..d9e369839c 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -418,6 +418,7 @@ mod tests { initial_rewarding_params: InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), sybil_resistance: Percent::from_percentage_value(23).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), @@ -456,6 +457,7 @@ mod tests { interval: IntervalRewardParams { reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), epoch_reward_budget: expected_epoch_reward_budget, stake_saturation_point: expected_stake_saturation_point, sybil_resistance: Percent::from_percentage_value(23).unwrap(), diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 02cdf83e68..894c260e6f 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -20,7 +20,8 @@ pub(crate) fn apply_reward_pool_changes( let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed + pending_pool_change.added; - let staking_supply = rewarding_params.interval.staking_supply + pending_pool_change.removed; + let staking_supply = rewarding_params.interval.staking_supply + + rewarding_params.interval.staking_supply_scale_factor * pending_pool_change.removed; let epoch_reward_budget = reward_pool / Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap() * rewarding_params.interval.interval_pool_emission; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 07b89671cd..a7cf4a3875 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -768,6 +768,7 @@ pub mod test_helpers { InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), sybil_resistance: Percent::from_percentage_value(30).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(2).unwrap(), diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e861e1b5fc..477ce47356 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -665,7 +665,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]]