Chore/remove unwraps (#1707)

* Disallowing the use of unwraps and expects in vesting and mixnet contracts

* Removed dodgy unwraps from the mixnet contract

* Removed dodgy unwraps from the vesting contract

* Removed unwraps/expects from common contracts crate

* ...but adding the unwraps in tests
This commit is contained in:
Jędrzej Stuczyński
2022-10-26 16:48:06 +01:00
committed by GitHub
parent caf03a09c8
commit 5ce087dafe
32 changed files with 569 additions and 278 deletions
+2
View File
@@ -0,0 +1,2 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
@@ -11,6 +11,8 @@ use cosmwasm_std::Event;
/// * `event`: event to search through.
/// * `key`: key associated with the particular attribute
pub fn must_find_attribute(event: &Event, key: &str) -> String {
// due to how the function is supposed to work, the unwrap is fine in this instance
#[allow(clippy::unwrap_used)]
may_find_attribute(event, key).unwrap()
}
@@ -1,6 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub mod events;
pub mod types;
@@ -1,7 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Decimal;
use cosmwasm_std::{Decimal, Uint128};
pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000);
// I'm still not 100% sure how to feel about existence of this file
// This is equivalent of representing our display coin with 6 decimal places.
@@ -4,8 +4,10 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::TOKEN_SUPPLY;
use crate::helpers::IntoBaseDecimal;
use crate::{Addr, MixId};
use cosmwasm_std::{Coin, Decimal};
use cosmwasm_std::{Coin, Decimal, StdResult};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -60,6 +62,11 @@ impl Delegation {
height: u64,
proxy: Option<Addr>,
) -> Self {
assert!(
amount.amount <= TOKEN_SUPPLY,
"delegation cannot be larger than the token supply"
);
Delegation {
owner,
mix_id,
@@ -87,10 +94,8 @@ impl Delegation {
(mix_id, owner_proxy_subkey)
}
pub fn dec_amount(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.amount.amount, 0).unwrap()
pub fn dec_amount(&self) -> StdResult<Decimal> {
self.amount.amount.into_base_decimal()
}
pub fn proxy_storage_key(&self) -> OwnerProxySubKey {
@@ -1,7 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Decimal;
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
@@ -11,3 +11,23 @@ pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
assert!(b - a < epsilon, "{} != {}", a, b)
}
}
pub fn into_base_decimal(val: impl Into<Uint128>) -> StdResult<Decimal> {
val.into_base_decimal()
}
pub trait IntoBaseDecimal {
fn into_base_decimal(self) -> StdResult<Decimal>;
}
impl<T> IntoBaseDecimal for T
where
T: Into<Uint128>,
{
fn into_base_decimal(self) -> StdResult<Decimal> {
let atomics = self.into();
Decimal::from_atomics(atomics, 0).map_err(|_| StdError::GenericErr {
msg: format!("Decimal range exceeded for {atomics} with 0 decimal places."),
})
}
}
@@ -142,14 +142,17 @@ impl JsonSchema for Interval {
impl Interval {
/// Initialize epoch in the contract with default values.
pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
// if this fails it means the value provided from the chain itself (via cosmwasm) is invalid,
// so we really have to panic here as anything beyond that point would be invalid anyway
#[allow(clippy::expect_used)]
let current_epoch_start =
OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64)
.expect("The timestamp provided via env.block.time is invalid");
Interval {
id: 0,
epochs_in_interval,
// I really don't see a way for this to fail, unless the blockchain is lying to us
current_epoch_start: OffsetDateTime::from_unix_timestamp(
env.block.time.seconds() as i64
)
.expect("Invalid timestamp from env.block.time"),
current_epoch_start,
current_epoch_id: 0,
epoch_length,
total_elapsed_epochs: 0,
@@ -1,6 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
mod constants;
pub mod delegation;
pub mod error;
@@ -4,13 +4,14 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::UNIT_DELEGATION_BASE;
use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE};
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::{NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution;
use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey};
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -64,7 +65,7 @@ impl MixNodeDetails {
self.rewarding_details.pending_operator_reward(pledge)
}
pub fn pending_detailed_operator_reward(&self) -> Decimal {
pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
let pledge = self.original_pledge();
self.rewarding_details
.pending_detailed_operator_reward(pledge)
@@ -107,16 +108,21 @@ impl MixNodeRewarding {
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Self {
MixNodeRewarding {
) -> Result<Self, MixnetContractError> {
assert!(
initial_pledge.amount <= TOKEN_SUPPLY,
"pledge cannot be larger than the token supply"
);
Ok(MixNodeRewarding {
cost_params,
operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(),
operator: initial_pledge.amount.into_base_decimal()?,
delegates: Decimal::zero(),
total_unit_reward: Decimal::zero(),
unit_delegation: UNIT_DELEGATION_BASE,
last_rewarded_epoch: current_epoch,
unique_delegations: 0,
}
})
}
/// Determines whether this node is still bonded. This is performed via a simple check,
@@ -135,27 +141,30 @@ impl MixNodeRewarding {
}
}
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
)
}
self.operator - initial_dec
Ok(self.operator - initial_dec)
}
pub fn operator_pledge_with_reward(&self, denom: impl Into<String>) -> Coin {
truncate_reward(self.operator, denom)
}
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin {
let delegator_reward = self.determine_delegation_reward(delegation);
truncate_reward(delegator_reward, &delegation.amount.denom)
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
let delegator_reward = self.determine_delegation_reward(delegation)?;
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
}
pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
) -> Result<Coin, MixnetContractError> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
@@ -164,14 +173,14 @@ impl MixNodeRewarding {
let diff = self.operator - initial_dec;
self.operator = initial_dec;
truncate_reward(diff, &original_pledge.denom)
Ok(truncate_reward(diff, &original_pledge.denom))
}
pub fn withdraw_delegator_reward(
&mut self,
delegation: &mut Delegation,
) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation);
let reward = self.determine_delegation_reward(delegation)?;
self.decrease_delegates_decimal(reward)?;
delegation.cumulative_reward_ratio = self.full_reward_ratio();
@@ -301,23 +310,27 @@ impl MixNodeRewarding {
self.distribute_rewards(reward_distribution, absolute_epoch_id)
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> StdResult<Decimal> {
let starting_ratio = delegation.cumulative_reward_ratio;
let ending_ratio = self.full_reward_ratio();
let adjust = starting_ratio + self.unit_delegation;
(ending_ratio - starting_ratio) * delegation.dec_amount() / adjust
Ok((ending_ratio - starting_ratio) * delegation.dec_amount()? / adjust)
}
// this updates `unique_delegations` field
pub fn add_base_delegation(&mut self, amount: Uint128) {
self.increase_delegates_uint128(amount);
pub fn add_base_delegation(&mut self, amount: Uint128) -> Result<(), MixnetContractError> {
self.increase_delegates_uint128(amount)?;
self.unique_delegations += 1;
Ok(())
}
pub fn increase_delegates_uint128(&mut self, amount: Uint128) {
// the unwrap here is fine as the value is guaranteed to fit under provided constraints
self.delegates += Decimal::from_atomics(amount, 0).unwrap()
pub fn increase_delegates_uint128(
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
self.delegates += amount.into_base_decimal()?;
Ok(())
}
// this updates `unique_delegations` field
@@ -335,7 +348,7 @@ impl MixNodeRewarding {
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
let amount_dec = Decimal::from_atomics(amount, 0).unwrap();
let amount_dec = amount.into_base_decimal()?;
self.decrease_delegates_decimal(amount_dec)
}
@@ -368,8 +381,8 @@ impl MixNodeRewarding {
}
pub fn undelegate(&mut self, delegation: &Delegation) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation);
let full_amount = reward + delegation.dec_amount();
let reward = self.determine_delegation_reward(delegation)?;
let full_amount = reward + delegation.dec_amount()?;
self.remove_delegation_decimal(full_amount)?;
Ok(truncate_reward(full_amount, &delegation.amount.denom))
}
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::delegation::OwnerProxySubKey;
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{
IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams,
@@ -40,14 +42,17 @@ pub struct InitialRewardingParams {
}
impl InitialRewardingParams {
pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams {
pub fn into_rewarding_params(
self,
epochs_in_interval: u32,
) -> Result<RewardingParams, MixnetContractError> {
let epoch_reward_budget = self.initial_reward_pool
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
/ epochs_in_interval.into_base_decimal()?
* self.interval_pool_emission;
let stake_saturation_point =
self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?;
RewardingParams {
Ok(RewardingParams {
interval: IntervalRewardParams {
reward_pool: self.initial_reward_pool,
staking_supply: self.initial_staking_supply,
@@ -59,7 +64,7 @@ impl InitialRewardingParams {
},
rewarded_set_size: self.rewarded_set_size,
active_set_size: self.active_set_size,
}
})
}
}
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::IntoBaseDecimal;
use crate::{error::MixnetContractError, Percent};
use cosmwasm_std::Decimal;
use schemars::JsonSchema;
@@ -105,24 +106,33 @@ impl RewardingParams {
pub fn dec_rewarded_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.rewarded_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
self.rewarded_set_size.into_base_decimal().unwrap()
}
pub fn dec_active_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.active_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
self.active_set_size.into_base_decimal().unwrap()
}
fn dec_standby_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap()
#[allow(clippy::unwrap_used)]
(self.rewarded_set_size - self.active_set_size)
.into_base_decimal()
.unwrap()
}
pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) {
self.interval.epoch_reward_budget = self.interval.reward_pool
/ Decimal::from_atomics(new_epochs_in_interval, 0).unwrap()
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
let new_epochs_in_interval = new_epochs_in_interval.into_base_decimal().unwrap();
self.interval.epoch_reward_budget = self.interval.reward_pool / new_epochs_in_interval
* self.interval.interval_pool_emission;
}
@@ -191,13 +201,13 @@ impl RewardingParams {
if recompute_epoch_budget {
self.interval.epoch_reward_budget = self.interval.reward_pool
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
/ epochs_in_interval.into_base_decimal()?
* self.interval.interval_pool_emission;
}
if recompute_saturation_point {
self.interval.stake_saturation_point = self.interval.staking_supply
/ Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
self.interval.stake_saturation_point =
self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()?
}
Ok(())
@@ -29,7 +29,7 @@ pub struct RewardEstimate {
pub operating_cost: Decimal,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
pub struct RewardDistribution {
pub operator: Decimal,
pub delegates: Decimal,
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::NodeRewardParams;
use crate::rewarding::simulator::simulated_node::SimulatedNode;
use crate::rewarding::RewardDistribution;
@@ -33,7 +34,7 @@ impl Simulator {
}
}
fn advance_epoch(&mut self) {
fn advance_epoch(&mut self) -> Result<(), MixnetContractError> {
let updated = self.interval.advance_epoch();
// we rolled over an interval
@@ -42,10 +43,13 @@ impl Simulator {
let reward_pool = old.reward_pool - self.pending_reward_pool_emission;
let staking_supply = old.staking_supply + self.pending_reward_pool_emission;
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
/ self.interval.epochs_in_interval().into_base_decimal()?
* old.interval_pool_emission.value();
let stake_saturation_point = staking_supply
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
/ self
.system_rewarding_params
.rewarded_set_size
.into_base_decimal()?;
let updated_params = RewardingParams {
interval: IntervalRewardParams {
@@ -65,9 +69,15 @@ impl Simulator {
self.pending_reward_pool_emission = Decimal::zero();
}
self.interval = updated;
Ok(())
}
pub fn bond(&mut self, pledge: Coin, cost_params: MixNodeCostParams) -> MixId {
pub fn bond(
&mut self,
pledge: Coin,
cost_params: MixNodeCostParams,
) -> Result<MixId, MixnetContractError> {
let mix_id = self.next_mix_id;
self.nodes.insert(
@@ -77,16 +87,24 @@ impl Simulator {
cost_params,
&pledge,
self.interval.current_epoch_absolute_id(),
),
)?,
);
self.next_mix_id += 1;
mix_id
Ok(mix_id)
}
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin, mix_id: MixId) {
let node = self.nodes.get_mut(&mix_id).expect("node doesn't exist");
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
mix_id: MixId,
) -> Result<(), MixnetContractError> {
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.delegate(delegator, delegation)
}
@@ -97,23 +115,35 @@ impl Simulator {
delegator: S,
mix_id: MixId,
) -> Result<(Coin, Coin), MixnetContractError> {
let node = self.nodes.get_mut(&mix_id).expect("node not found");
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.undelegate(delegator)
}
pub fn simulate_epoch_single_node(&mut self, params: NodeRewardParams) -> RewardDistribution {
pub fn simulate_epoch_single_node(
&mut self,
params: NodeRewardParams,
) -> Result<RewardDistribution, MixnetContractError> {
assert_eq!(self.nodes.len(), 1);
let id = *self.nodes.keys().next().unwrap();
let mut params_map = BTreeMap::new();
params_map.insert(id, params);
self.simulate_epoch(&params_map).remove(&id).unwrap()
if let Some(&id) = self.nodes.keys().next() {
let mut params_map = BTreeMap::new();
params_map.insert(id, params);
Ok(self
.simulate_epoch(&params_map)?
.remove(&id)
.unwrap_or_default())
} else {
Ok(RewardDistribution::default())
}
}
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> BTreeMap<MixId, RewardDistribution> {
) -> Result<BTreeMap<MixId, RewardDistribution>, MixnetContractError> {
let mut params_keys = node_params.keys().copied().collect::<Vec<_>>();
params_keys.sort_unstable();
let mut node_keys = self.nodes.keys().copied().collect::<Vec<_>>();
@@ -141,34 +171,41 @@ impl Simulator {
dist.insert(*mix_id, reward_distribution);
}
self.advance_epoch();
dist
self.advance_epoch()?;
Ok(dist)
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
self.nodes[&delegation.mix_id]
pub fn determine_delegation_reward(
&self,
delegation: &Delegation,
) -> Result<Decimal, MixnetContractError> {
Ok(self.nodes[&delegation.mix_id]
.rewarding_details
.determine_delegation_reward(delegation)
.determine_delegation_reward(delegation)?)
}
pub fn determine_total_delegation_reward(&self) -> Decimal {
pub fn determine_total_delegation_reward(&self) -> Result<Decimal, MixnetContractError> {
let mut total = Decimal::zero();
for node in self.nodes.values() {
for delegation in node.delegations.values() {
total += node
.rewarding_details
.determine_delegation_reward(delegation)
.determine_delegation_reward(delegation)?
}
}
total
Ok(total)
}
// assume node state doesn't change in the interval (kinda unrealistic)
pub fn simulate_full_interval(&mut self, node_params: &BTreeMap<MixId, NodeRewardParams>) {
pub fn simulate_full_interval(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> Result<(), MixnetContractError> {
for _ in 0..self.interval.epochs_in_interval() {
self.simulate_epoch(node_params);
self.simulate_epoch(node_params)?;
}
Ok(())
}
}
@@ -231,20 +268,27 @@ mod tests {
profit_margin_percent: profit_margin,
interval_operating_cost,
};
simulator.bond(initial_pledge, cost_params);
simulator.bond(initial_pledge, cost_params).unwrap();
simulator
}
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
fn check_rewarding_invariant(simulator: &Simulator) {
for node in simulator.nodes.values() {
let delegation_sum: Decimal =
node.delegations.values().map(|d| d.dec_amount()).sum();
let delegation_sum: Decimal = node
.delegations
.values()
.map(|d| d.dec_amount().unwrap())
.sum();
let reward_sum: Decimal = node
.delegations
.values()
.map(|d| node.rewarding_details.determine_delegation_reward(d))
.map(|d| {
node.rewarding_details
.determine_delegation_reward(d)
.unwrap()
})
.sum();
// let reward_sum = simulator.determine_total_delegation_reward();
@@ -262,7 +306,7 @@ mod tests {
let epoch_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(epoch_params);
let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap();
assert_eq!(rewards.delegates, Decimal::zero());
compare_decimals(
@@ -275,11 +319,13 @@ mod tests {
#[test]
fn single_delegation_at_genesis() {
let mut simulator = base_simulator(10000_000000);
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(node_params);
let rewards = simulator.simulate_epoch_single_node(node_params).unwrap();
compare_decimals(
rewards.delegates,
@@ -290,7 +336,7 @@ mod tests {
compare_decimals(
rewards.delegates,
simulator.determine_total_delegation_reward(),
simulator.determine_total_delegation_reward().unwrap(),
None,
);
let node = &simulator.nodes[&0];
@@ -310,20 +356,22 @@ mod tests {
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards1 = simulator.simulate_epoch_single_node(node_params);
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1128452.5416104363".parse().unwrap();
assert_eq!(rewards1.delegates, Decimal::zero());
compare_decimals(rewards1.operator, expected_operator1, None);
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let rewards2 = simulator.simulate_epoch_single_node(node_params);
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1363843.413584609".parse().unwrap();
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward1, None);
compare_decimals(rewards2.operator, expected_operator2, None);
let rewards3 = simulator.simulate_epoch_single_node(node_params);
let rewards3 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator3 = "1364017.7824440491".parse().unwrap();
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
compare_decimals(rewards3.delegates, expected_delegator_reward2, None);
@@ -357,11 +405,15 @@ mod tests {
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params);
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
@@ -371,14 +423,15 @@ mod tests {
let node = simulator.nodes.get_mut(&0).unwrap();
let reward = node
.rewarding_details
.withdraw_operator_reward(&original_pledge);
.withdraw_operator_reward(&original_pledge)
.unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
assert_eq!(
node.rewarding_details.operator,
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
);
let rewards2 = simulator.simulate_epoch_single_node(node_params);
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411113.0004067947".parse().unwrap();
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
@@ -395,11 +448,15 @@ mod tests {
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params);
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
@@ -417,7 +474,7 @@ mod tests {
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
// new reward after withdrawal
let rewards2 = simulator.simulate_epoch_single_node(node_params);
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411250.1907492676".parse().unwrap();
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
@@ -460,22 +517,30 @@ mod tests {
let mut performance = Percent::from_percentage_value(100).unwrap();
for epoch in 0..720 {
if epoch == 0 {
simulator.delegate("a", Coin::new(18000_000000, "unym"), 0)
simulator
.delegate("a", Coin::new(18000_000000, "unym"), 0)
.unwrap()
}
if epoch == 42 {
simulator.delegate("b", Coin::new(2000_000000, "unym"), 0)
simulator
.delegate("b", Coin::new(2000_000000, "unym"), 0)
.unwrap()
}
if epoch == 89 {
is_active = false;
}
if epoch == 123 {
simulator.delegate("c", Coin::new(6666_000000, "unym"), 0)
simulator
.delegate("c", Coin::new(6666_000000, "unym"), 0)
.unwrap()
}
if epoch == 167 {
performance = Percent::from_percentage_value(90).unwrap();
}
if epoch == 245 {
simulator.delegate("d", Coin::new(2050_000000, "unym"), 0)
simulator
.delegate("d", Coin::new(2050_000000, "unym"), 0)
.unwrap()
}
if epoch == 264 {
let (delegation, _reward) = simulator.undelegate("b", 0).unwrap();
@@ -496,13 +561,15 @@ mod tests {
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 545 {
simulator.delegate("e", Coin::new(5000_000000, "unym"), 0)
simulator
.delegate("e", Coin::new(5000_000000, "unym"), 0)
.unwrap()
}
// this has to always hold
check_rewarding_invariant(&simulator);
let node_params = NodeRewardParams::new(performance, is_active);
simulator.simulate_epoch_single_node(node_params);
simulator.simulate_epoch_single_node(node_params).unwrap();
}
// after everyone undelegates, there should be nothing left in the delegates pool
@@ -555,95 +622,135 @@ mod tests {
let mut simulator = Simulator::new(rewarding_params, interval);
let n0 = simulator.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0);
let n0 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0)
.unwrap();
let n1 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1);
let n1 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1)
.unwrap();
let n2 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2);
let n2 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2)
.unwrap();
let n3 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3);
let n3 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3)
.unwrap();
let n4 = simulator.bond(
Coin::new(1000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4);
let n4 = simulator
.bond(
Coin::new(1000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4)
.unwrap();
let n5 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5);
let n5 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5)
.unwrap();
let n6 = simulator.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6);
let n6 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6)
.unwrap();
let n7 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7);
let n7 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7)
.unwrap();
let n8 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8);
let n8 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8)
.unwrap();
let n9 = simulator.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
);
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9);
let n9 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9)
.unwrap();
let uptime_1 = Percent::from_percentage_value(100).unwrap();
let uptime_09 = Percent::from_percentage_value(90).unwrap();
@@ -665,7 +772,7 @@ mod tests {
.collect::<BTreeMap<_, _>>();
for _ in 0..23 {
simulator.simulate_full_interval(&node_params);
simulator.simulate_full_interval(&node_params).unwrap();
}
// we allow the delta to be within 0.1unym,
@@ -20,21 +20,25 @@ impl SimulatedNode {
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Self {
SimulatedNode {
) -> Result<Self, MixnetContractError> {
Ok(SimulatedNode {
mix_id,
rewarding_details: MixNodeRewarding::initialise_new(
cost_params,
initial_pledge,
current_epoch,
),
)?,
delegations: HashMap::new(),
}
})
}
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin) {
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
) -> Result<(), MixnetContractError> {
self.rewarding_details
.add_base_delegation(delegation.amount);
.add_base_delegation(delegation.amount)?;
let delegator = delegator.into();
let delegation = Delegation::new(
@@ -47,6 +51,7 @@ impl SimulatedNode {
);
self.delegations.insert(delegator, delegation);
Ok(())
}
pub fn undelegate<S: Into<String>>(
@@ -54,16 +59,19 @@ impl SimulatedNode {
delegator: S,
) -> Result<(Coin, Coin), MixnetContractError> {
let delegator = delegator.into();
let delegation = self
.delegations
.remove(&delegator)
.expect("delegation not found");
let delegation = self.delegations.remove(&delegator).ok_or(
MixnetContractError::NoMixnodeDelegationFound {
mix_id: MixId::MAX,
address: delegator,
proxy: None,
},
)?;
let reward = self
.rewarding_details
.determine_delegation_reward(&delegation);
.determine_delegation_reward(&delegation)?;
self.rewarding_details
.remove_delegation_decimal(delegation.dec_amount() + reward)?;
.remove_delegation_decimal(delegation.dec_amount()? + reward)?;
let reward_denom = &delegation.amount.denom;
let truncated_reward = truncate_reward(reward, reward_denom);
@@ -61,7 +61,7 @@ impl Percent {
impl Display for Percent {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
write!(f, "{}%", adjusted)
}
}
@@ -119,6 +119,10 @@ impl LayerDistribution {
(Layer::Two, self.layer2),
(Layer::Three, self.layer3),
];
// we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY
// not empty and thus the unwrap cannot fail
#[allow(clippy::unwrap_used)]
layers.iter().min_by_key(|x| x.1).unwrap().0
}
@@ -1,5 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use mixnet_contract_common::MixId;
use schemars::JsonSchema;
+2
View File
@@ -0,0 +1,2 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
+1 -1
View File
@@ -63,7 +63,7 @@ pub fn instantiate(
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
let reward_params = msg
.initial_rewarding_params
.into_rewarding_params(msg.epochs_in_interval);
.into_rewarding_params(msg.epochs_in_interval)?;
interval_storage::initialise_storage(deps.storage, starting_interval)?;
mixnet_params_storage::initialise_storage(deps.storage, state)?;
@@ -107,7 +107,7 @@ pub(crate) fn delegate(
};
// add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one)
mix_rewarding.add_base_delegation(stored_delegation_amount.amount);
mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?;
let cosmos_event = new_delegation_event(
created_at,
+3
View File
@@ -1,6 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
mod constants;
pub mod contract;
mod delegations;
+1 -1
View File
@@ -78,7 +78,7 @@ pub(crate) fn save_new_mixnode(
let mix_id = next_mixnode_id_counter(storage)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch);
let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
let mixnode_bond = MixNodeBond::new(
mix_id,
owner,
+18 -12
View File
@@ -4,8 +4,9 @@
use super::storage;
use crate::delegations::storage as delegations_storage;
use crate::interval::storage as interval_storage;
use cosmwasm_std::{Coin, Decimal, Storage};
use cosmwasm_std::{Coin, Storage};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::helpers::IntoBaseDecimal;
use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding};
use mixnet_contract_common::Delegation;
@@ -21,11 +22,10 @@ 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 epoch_reward_budget = reward_pool
/ Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap()
let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()?
* rewarding_params.interval.interval_pool_emission;
let stake_saturation_point =
staking_supply / Decimal::from_atomics(rewarding_params.rewarded_set_size, 0).unwrap();
staking_supply / rewarding_params.rewarded_set_size.into_base_decimal()?;
rewarding_params.interval.reward_pool = reward_pool;
rewarding_params.interval.staking_supply = staking_supply;
@@ -45,7 +45,7 @@ pub(crate) fn withdraw_operator_reward(
let mix_id = mix_details.mix_id();
let mut mix_rewarding = mix_details.rewarding_details;
let original_pledge = mix_details.bond_information.original_pledge;
let reward = mix_rewarding.withdraw_operator_reward(&original_pledge);
let reward = mix_rewarding.withdraw_operator_reward(&original_pledge)?;
// save updated rewarding info
storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?;
@@ -86,7 +86,7 @@ mod tests {
let mut test = TestSetup::new();
let epochs_in_interval = test.current_interval().epochs_in_interval();
let epochs_in_interval_dec = Decimal::from_atomics(epochs_in_interval, 0).unwrap();
let epochs_in_interval_dec = epochs_in_interval.into_base_decimal().unwrap();
let start_rewarding_params = test.rewarding_params();
// nothing changes if pending changes are empty
@@ -94,7 +94,7 @@ mod tests {
assert_eq!(start_rewarding_params, test.rewarding_params());
// normal case of having distributed some rewards
let distributed_rewards = Decimal::from_atomics(100_000_000u32, 0).unwrap();
let distributed_rewards = 100_000_000u32.into_base_decimal().unwrap();
storage::PENDING_REWARD_POOL_CHANGE
.save(
test.deps_mut().storage,
@@ -131,7 +131,10 @@ mod tests {
assert_eq!(
updated_rewarding_params.interval.stake_saturation_point,
updated_rewarding_params.interval.staking_supply
/ Decimal::from_atomics(updated_rewarding_params.rewarded_set_size, 0).unwrap()
/ updated_rewarding_params
.rewarded_set_size
.into_base_decimal()
.unwrap()
);
// resets changes back to 0
@@ -143,7 +146,7 @@ mod tests {
);
// future case of having to also increase the reward pool
let added_credentials = Decimal::from_atomics(50_000_000u32, 0).unwrap();
let added_credentials = 50_000_000u32.into_base_decimal().unwrap();
storage::PENDING_REWARD_POOL_CHANGE
.save(
test.deps_mut().storage,
@@ -180,7 +183,10 @@ mod tests {
assert_eq!(
updated_rewarding_params2.interval.stake_saturation_point,
updated_rewarding_params2.interval.staking_supply
/ Decimal::from_atomics(updated_rewarding_params2.rewarded_set_size, 0).unwrap()
/ updated_rewarding_params2
.rewarded_set_size
.into_base_decimal()
.unwrap()
);
// resets changes back to 0
@@ -197,7 +203,7 @@ mod tests {
let mut test = TestSetup::new();
let pledge = Uint128::new(250_000_000);
let pledge_dec = Decimal::from_atomics(250_000_000u32, 0).unwrap();
let pledge_dec = 250_000_000u32.into_base_decimal().unwrap();
let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge));
// no rewards
@@ -234,7 +240,7 @@ mod tests {
let mut test = TestSetup::new();
let delegation_amount = Uint128::new(2500_000_000);
let delegation_dec = Decimal::from_atomics(2500_000_000u32, 0).unwrap();
let delegation_dec = 2500_000_000u32.into_base_decimal().unwrap();
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let delegator = "delegator";
test.add_immediate_delegation(delegator, delegation_amount, mix_id);
+17 -11
View File
@@ -7,6 +7,7 @@ use crate::interval::storage as interval_storage;
use crate::mixnodes;
use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult};
use mixnet_contract_common::helpers::into_base_decimal;
use mixnet_contract_common::mixnode::MixNodeDetails;
use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams};
use mixnet_contract_common::rewarding::helpers::truncate_reward;
@@ -19,16 +20,18 @@ pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult<RewardingParam
storage::REWARDING_PARAMS.load(deps.storage)
}
fn pending_operator_reward(mix_details: Option<MixNodeDetails>) -> PendingRewardResponse {
match mix_details {
fn pending_operator_reward(
mix_details: Option<MixNodeDetails>,
) -> StdResult<PendingRewardResponse> {
Ok(match mix_details {
Some(mix_details) => PendingRewardResponse {
amount_staked: Some(mix_details.original_pledge().clone()),
amount_earned: Some(mix_details.pending_operator_reward()),
amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()),
amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()?),
mixnode_still_fully_bonded: !mix_details.is_unbonding(),
},
None => PendingRewardResponse::default(),
}
})
}
pub fn query_pending_operator_reward(
@@ -39,7 +42,7 @@ pub fn query_pending_operator_reward(
// in order to determine operator's reward we need to know its original pledge and thus
// we have to load the entire thing
let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?;
Ok(pending_operator_reward(mix_details))
pending_operator_reward(mix_details)
}
pub fn query_pending_mixnode_operator_reward(
@@ -49,7 +52,7 @@ pub fn query_pending_mixnode_operator_reward(
// in order to determine operator's reward we need to know its original pledge and thus
// we have to load the entire thing
let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?;
Ok(pending_operator_reward(mix_details))
pending_operator_reward(mix_details)
}
pub fn query_pending_delegator_reward(
@@ -74,8 +77,8 @@ pub fn query_pending_delegator_reward(
None => return Ok(PendingRewardResponse::default()),
};
let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation);
let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation);
let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation)?;
let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation)?;
// check if the mixnode isnt in the process of unbonding (or has already unbonded)
let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding);
@@ -177,8 +180,8 @@ pub(crate) fn query_estimated_current_epoch_delegator_reward(
None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()),
};
let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap();
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation);
let staked_dec = into_base_decimal(delegation.amount.amount)?;
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation)?;
let amount_staked = delegation.amount;
// check if the mixnode isnt in the process of unbonding (or has already unbonded)
@@ -792,7 +795,10 @@ mod tests {
let delegation = test.delegation(mix_id, owner, &None);
let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap();
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation);
let current_value = staked_dec
+ mix_rewarding
.determine_delegation_reward(&delegation)
.unwrap();
let amount_staked = delegation.amount;
EstimatedCurrentEpochRewardResponse {
+30 -22
View File
@@ -744,7 +744,7 @@ pub mod tests {
performance,
in_active_set: true,
};
let sim_res = sim.simulate_epoch_single_node(node_params);
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
assert_eq!(sim_res, dist);
}
test.skip_to_next_epoch_end();
@@ -768,7 +768,7 @@ pub mod tests {
performance,
in_active_set: true,
};
let sim_res = sim.simulate_epoch_single_node(node_params);
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
assert_eq!(sim_res, dist);
}
test.skip_to_next_epoch_end();
@@ -809,8 +809,8 @@ pub mod tests {
in_active_set: true,
};
let dist1 = sim1.simulate_epoch_single_node(node_params);
let dist2 = sim2.simulate_epoch_single_node(node_params);
let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap();
let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap();
let env = test.env();
@@ -858,15 +858,17 @@ pub mod tests {
let unit_delegation_base = actual_prior1.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del11 = del11.dec_amount()
+ (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount()
let pre_rewarding_del11 = del11.dec_amount().unwrap()
+ (prior_unit_reward - del11.cumulative_reward_ratio)
* del11.dec_amount().unwrap()
/ (del11.cumulative_reward_ratio + unit_delegation_base);
let computed_del11_reward =
pre_rewarding_del11 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del12 = del12.dec_amount()
+ (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount()
let pre_rewarding_del12 = del12.dec_amount().unwrap()
+ (prior_unit_reward - del12.cumulative_reward_ratio)
* del12.dec_amount().unwrap()
/ (del12.cumulative_reward_ratio + unit_delegation_base);
let computed_del12_reward =
@@ -920,8 +922,9 @@ pub mod tests {
let unit_delegation_base = actual_prior2.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del21 = del21.dec_amount()
+ (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount()
let pre_rewarding_del21 = del21.dec_amount().unwrap()
+ (prior_unit_reward - del21.cumulative_reward_ratio)
* del21.dec_amount().unwrap()
/ (del21.cumulative_reward_ratio + unit_delegation_base);
let computed_del21_reward =
@@ -949,8 +952,8 @@ pub mod tests {
in_active_set: true,
};
let dist1 = sim1.simulate_epoch_single_node(node_params);
let dist2 = sim2.simulate_epoch_single_node(node_params);
let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap();
let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap();
let env = test.env();
@@ -998,22 +1001,25 @@ pub mod tests {
let unit_delegation_base = actual_prior1.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del11 = del11.dec_amount()
+ (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount()
let pre_rewarding_del11 = del11.dec_amount().unwrap()
+ (prior_unit_reward - del11.cumulative_reward_ratio)
* del11.dec_amount().unwrap()
/ (del11.cumulative_reward_ratio + unit_delegation_base);
let computed_del11_reward =
pre_rewarding_del11 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del12 = del12.dec_amount()
+ (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount()
let pre_rewarding_del12 = del12.dec_amount().unwrap()
+ (prior_unit_reward - del12.cumulative_reward_ratio)
* del12.dec_amount().unwrap()
/ (del12.cumulative_reward_ratio + unit_delegation_base);
let computed_del12_reward =
pre_rewarding_del12 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del13 = del13.dec_amount()
+ (prior_unit_reward - del13.cumulative_reward_ratio) * del13.dec_amount()
let pre_rewarding_del13 = del13.dec_amount().unwrap()
+ (prior_unit_reward - del13.cumulative_reward_ratio)
* del13.dec_amount().unwrap()
/ (del13.cumulative_reward_ratio + unit_delegation_base);
let computed_del13_reward =
@@ -1067,15 +1073,17 @@ pub mod tests {
let unit_delegation_base = actual_prior2.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del21 = del21.dec_amount()
+ (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount()
let pre_rewarding_del21 = del21.dec_amount().unwrap()
+ (prior_unit_reward - del21.cumulative_reward_ratio)
* del21.dec_amount().unwrap()
/ (del21.cumulative_reward_ratio + unit_delegation_base);
let computed_del21_reward =
pre_rewarding_del21 / prior_delegates2 * delegates_reward2;
let pre_rewarding_del23 = del23.dec_amount()
+ (prior_unit_reward - del23.cumulative_reward_ratio) * del23.dec_amount()
let pre_rewarding_del23 = del23.dec_amount().unwrap()
+ (prior_unit_reward - del23.cumulative_reward_ratio)
* del23.dec_amount().unwrap()
/ (del23.cumulative_reward_ratio + unit_delegation_base);
let computed_del23_reward =
+8
View File
@@ -69,6 +69,10 @@ pub(crate) fn validate_pledge(
});
}
// throughout this function we've been using the value at `pledge[0]` without problems
// (plus we have even validated that the vec is not empty), so the unwrap here is absolutely fine,
// since it cannot possibly fail without UB
#[allow(clippy::unwrap_used)]
Ok(pledge.pop().unwrap())
}
@@ -106,6 +110,10 @@ pub(crate) fn validate_delegation_stake(
return Err(MixnetContractError::EmptyDelegation);
}
// throughout this function we've been using the value at `delegation[0]` without problems
// (plus we have even validated that the vec is not empty), so the unwrap here is absolutely fine,
// since it cannot possibly fail without UB
#[allow(clippy::unwrap_used)]
Ok(delegation.pop().unwrap())
}
+2 -2
View File
@@ -579,7 +579,7 @@ pub fn try_get_current_vesting_period(
env: Env,
) -> Result<Period, ContractError> {
let account = account_from_address(address, deps.storage, deps.api)?;
Ok(account.get_current_vesting_period(env.block.time))
account.get_current_vesting_period(env.block.time)
}
/// Loads mixnode bond from vesting contract storage.
@@ -666,7 +666,7 @@ pub fn try_get_original_vesting(
deps: Deps<'_>,
) -> Result<OriginalVestingResponse, ContractError> {
let account = account_from_address(vesting_account_address, deps.storage, deps.api)?;
Ok(account.get_original_vesting())
account.get_original_vesting()
}
/// See [crate::traits::VestingAccount::get_delegated_free]
+2
View File
@@ -48,4 +48,6 @@ pub enum ContractError {
MinVestingFunds { sent: u128, need: u128 },
#[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())]
LockedPledgeCapReached { current: Uint128, cap: Uint128 },
#[error("VESTING: ({}: Account owned by {owner} has unpopulated vesting periods!", line!())]
UnpopulatedVestingPeriods { owner: Addr },
}
+3
View File
@@ -1,6 +1,9 @@
#![allow(rustdoc::private_intra_doc_links)]
//! Nym vesting contract, providing vesting accounts with ability to stake unvested tokens
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub mod contract;
mod errors;
mod queued_migrations;
@@ -55,7 +55,7 @@ pub trait VestingAccount {
/// Returns amount of coins set at account creation
/// See [/vesting-contract/struct.Account.html/method.get_original_vesting] for impl
fn get_original_vesting(&self) -> OriginalVestingResponse;
fn get_original_vesting(&self) -> Result<OriginalVestingResponse, ContractError>;
/// See [/vesting-contract/struct.Account.html/method.get_delegated_free] for impl
fn get_delegated_free(
+30 -8
View File
@@ -65,8 +65,13 @@ impl Account {
self.periods.len()
}
pub fn period_duration(&self) -> u64 {
self.periods.get(0).unwrap().period_seconds
pub fn period_duration(&self) -> Result<u64, ContractError> {
self.periods
.get(0)
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})
.map(|p| p.period_seconds)
}
pub fn storage_key(&self) -> u32 {
@@ -103,11 +108,28 @@ impl Account {
/// Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet.
/// In case vesting is over it will always return NUM_VESTING_PERIODS.
pub fn get_current_vesting_period(&self, block_time: Timestamp) -> Period {
if block_time.seconds() < self.periods.first().unwrap().start_time {
Period::Before
} else if self.periods.last().unwrap().end_time() < block_time {
Period::After
pub fn get_current_vesting_period(
&self,
block_time: Timestamp,
) -> Result<Period, ContractError> {
let first_period =
self.periods
.first()
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})?;
let last_period = self
.periods
.last()
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})?;
if block_time.seconds() < first_period.start_time {
Ok(Period::Before)
} else if last_period.end_time() < block_time {
Ok(Period::After)
} else {
let mut index = 0;
for period in &self.periods {
@@ -116,7 +138,7 @@ impl Account {
}
index += 1;
}
Period::In(index)
Ok(Period::In(index))
}
}
@@ -67,7 +67,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time);
let period = self.get_current_vesting_period(block_time)?;
let denom = MIX_DENOM.load(storage)?;
let amount = match period {
@@ -94,7 +94,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
Ok(Coin {
amount: self.get_original_vesting().amount().amount
amount: self.get_original_vesting()?.amount().amount
- self.get_vested_coins(block_time, env, storage)?.amount,
denom: MIX_DENOM.load(storage)?,
})
@@ -108,12 +108,12 @@ impl VestingAccount for Account {
self.periods[(self.num_vesting_periods() - 1) as usize].end_time()
}
fn get_original_vesting(&self) -> OriginalVestingResponse {
OriginalVestingResponse::new(
fn get_original_vesting(&self) -> Result<OriginalVestingResponse, ContractError> {
Ok(OriginalVestingResponse::new(
self.coin.clone(),
self.num_vesting_periods(),
self.period_duration(),
)
self.period_duration()?,
))
}
fn get_delegated_free(
@@ -123,7 +123,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time);
let period = self.get_current_vesting_period(block_time)?;
let withdrawn = self.load_withdrawn(storage)?;
let max_available = self
.get_vested_coins(Some(block_time), env, storage)?
@@ -155,7 +155,7 @@ impl VestingAccount for Account {
let block_time = block_time.unwrap_or(env.block.time);
let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?;
let period = self.get_current_vesting_period(block_time);
let period = self.get_current_vesting_period(block_time)?;
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
@@ -180,7 +180,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time);
let period = self.get_current_vesting_period(block_time)?;
let max_vested = self.get_vested_coins(Some(block_time), env, storage)?;
let start_time = match period {
Period::Before => 0,
+51 -11
View File
@@ -180,12 +180,14 @@ mod tests {
assert_eq!(account.periods().len(), num_vesting_periods as usize);
let current_period = account.get_current_vesting_period(Timestamp::from_seconds(0));
let current_period = account
.get_current_vesting_period(Timestamp::from_seconds(0))
.unwrap();
assert_eq!(Period::Before, current_period);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time);
let current_period = account.get_current_vesting_period(block_time).unwrap();
assert_eq!(current_period, Period::In(1));
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -196,21 +198,37 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
account.get_original_vesting().amount().amount.u128() / num_vesting_periods as u128
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
/ num_vesting_periods as u128
)
);
assert_eq!(
vesting_coins.amount,
Uint128::new(
account.get_original_vesting().amount().amount.u128()
- account.get_original_vesting().amount().amount.u128()
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
- account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
/ num_vesting_periods as u128
)
);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + 5 * vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time);
let current_period = account.get_current_vesting_period(block_time).unwrap();
assert_eq!(current_period, Period::In(5));
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -221,15 +239,30 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
5 * account.get_original_vesting().amount().amount.u128()
5 * account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
/ num_vesting_periods as u128
)
);
assert_eq!(
vesting_coins.amount,
Uint128::new(
account.get_original_vesting().amount().amount.u128()
- 5 * account.get_original_vesting().amount().amount.u128()
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
- 5 * account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
/ num_vesting_periods as u128
)
);
@@ -237,7 +270,7 @@ mod tests {
let block_time = Timestamp::from_seconds(
account.start_time().seconds() + vesting_over_period * vesting_period + 1,
);
let current_period = account.get_current_vesting_period(block_time);
let current_period = account.get_current_vesting_period(block_time).unwrap();
assert_eq!(current_period, Period::After);
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -247,7 +280,14 @@ mod tests {
.unwrap();
assert_eq!(
vested_coins.amount,
Uint128::new(account.get_original_vesting().amount().amount.u128())
Uint128::new(
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
)
);
assert_eq!(vesting_coins.amount, Uint128::zero());
}