added unit tests for vested migrations and fixed additional issues
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
|
||||
|
||||
#[track_caller]
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
|
||||
if a > b {
|
||||
|
||||
Generated
+4
-2
@@ -1283,6 +1283,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -1748,11 +1749,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -45,6 +45,7 @@ time = { version = "0.3", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3"
|
||||
rand = "0.8.5"
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -273,7 +273,7 @@ pub fn execute(
|
||||
crate::vesting_migration::try_migrate_vested_mixnode(deps, info)
|
||||
}
|
||||
ExecuteMsg::MigrateVestedDelegation { mix_id } => {
|
||||
crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id)
|
||||
crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id)
|
||||
}
|
||||
|
||||
// legacy vesting
|
||||
|
||||
@@ -24,9 +24,10 @@ pub mod test_helpers {
|
||||
minimum_gateway_pledge, minimum_mixnode_pledge, rewarding_denom,
|
||||
rewarding_validator_address,
|
||||
};
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::mixnodes::storage::mixnode_bonds;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
||||
use crate::mixnodes::storage::{assign_layer, mixnode_bonds, next_mixnode_id_counter};
|
||||
use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode};
|
||||
use crate::mixnodes::{storage as mixnodes_storage, storage};
|
||||
use crate::rewards::queries::{
|
||||
query_pending_delegator_reward, query_pending_mixnode_operator_reward,
|
||||
};
|
||||
@@ -42,7 +43,7 @@ pub mod test_helpers {
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{Coin, Order};
|
||||
use cosmwasm_std::{Decimal, Empty, MemoryStorage};
|
||||
use cosmwasm_std::{Deps, OwnedDeps};
|
||||
@@ -120,6 +121,10 @@ pub mod test_helpers {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn random_address(&mut self) -> String {
|
||||
format!("n1foomp{}", self.rng.next_u64())
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.deps.as_ref()
|
||||
}
|
||||
@@ -146,6 +151,13 @@ pub mod test_helpers {
|
||||
self.owner.clone()
|
||||
}
|
||||
|
||||
pub fn vesting_contract(&self) -> Addr {
|
||||
mixnet_params_storage::CONTRACT_STATE
|
||||
.load(self.deps().storage)
|
||||
.unwrap()
|
||||
.vesting_contract_address
|
||||
}
|
||||
|
||||
pub fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, rewarding_denom(self.deps().storage).unwrap())
|
||||
}
|
||||
@@ -266,6 +278,72 @@ pub mod test_helpers {
|
||||
current_id_counter + 1
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_proxy_and_keypair(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> (MixId, identity::KeyPair) {
|
||||
let pledge = self.make_mix_pledge(stake).pop().unwrap();
|
||||
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let keypair = identity::KeyPair::new(&mut self.rng);
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng);
|
||||
|
||||
let mixnode = MixNode {
|
||||
identity_key,
|
||||
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
|
||||
..tests::fixtures::mix_node_fixture()
|
||||
};
|
||||
|
||||
let height = self.env.block.height;
|
||||
let storage = self.deps_mut().storage;
|
||||
|
||||
// manually unroll `save_new_mixnode` to allow for proxy usage
|
||||
let layer = assign_layer(storage).unwrap();
|
||||
let mix_id = next_mixnode_id_counter(storage).unwrap();
|
||||
|
||||
let current_epoch = interval_storage::current_interval(storage)
|
||||
.unwrap()
|
||||
.current_epoch_absolute_id();
|
||||
|
||||
let mixnode_rewarding = MixNodeRewarding::initialise_new(
|
||||
tests::fixtures::mix_node_cost_params_fixture(),
|
||||
&pledge,
|
||||
current_epoch,
|
||||
)
|
||||
.unwrap();
|
||||
let mixnode_bond = MixNodeBond {
|
||||
mix_id,
|
||||
owner: Addr::unchecked(owner),
|
||||
original_pledge: pledge,
|
||||
layer,
|
||||
mix_node: mixnode,
|
||||
proxy: Some(proxy),
|
||||
bonding_height: height,
|
||||
is_unbonding: false,
|
||||
};
|
||||
|
||||
mixnode_bonds()
|
||||
.save(storage, mix_id, &mixnode_bond)
|
||||
.unwrap();
|
||||
rewards_storage::MIXNODE_REWARDING
|
||||
.save(storage, mix_id, &mixnode_rewarding)
|
||||
.unwrap();
|
||||
|
||||
(mix_id, keypair)
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_legal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> MixId {
|
||||
self.add_dummy_mixnode_with_proxy_and_keypair(owner, stake)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option<Uint128>) -> IdentityKey {
|
||||
let stake = self.make_gateway_pledge(stake);
|
||||
let (gateway, owner_signature) =
|
||||
@@ -454,6 +532,55 @@ pub mod test_helpers {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn add_immediate_delegation_with_legal_proxy(
|
||||
&mut self,
|
||||
delegator: &str,
|
||||
amount: impl Into<Uint128>,
|
||||
target: MixId,
|
||||
) {
|
||||
let denom = rewarding_denom(self.deps().storage).unwrap();
|
||||
let amount = Coin {
|
||||
denom,
|
||||
amount: amount.into(),
|
||||
};
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let owner = self.deps.api.addr_validate(delegator).unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy));
|
||||
|
||||
let mut mix_rewarding = self.mix_rewarding(target);
|
||||
|
||||
let mut stored_delegation_amount = amount;
|
||||
|
||||
if let Some(existing_delegation) = delegations_storage::delegations()
|
||||
.may_load(&self.deps.storage, storage_key.clone())
|
||||
.unwrap()
|
||||
{
|
||||
let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap();
|
||||
stored_delegation_amount.amount += og_with_reward.amount;
|
||||
}
|
||||
|
||||
mix_rewarding
|
||||
.add_base_delegation(stored_delegation_amount.amount)
|
||||
.unwrap();
|
||||
|
||||
let delegation = Delegation {
|
||||
owner,
|
||||
mix_id: target,
|
||||
cumulative_reward_ratio: mix_rewarding.total_unit_reward,
|
||||
amount: stored_delegation_amount,
|
||||
height: self.env.block.height,
|
||||
proxy: Some(proxy),
|
||||
};
|
||||
|
||||
delegations_storage::delegations()
|
||||
.save(&mut self.deps.storage, storage_key, &delegation)
|
||||
.unwrap();
|
||||
rewards_storage::MIXNODE_REWARDING
|
||||
.save(&mut self.deps.storage, target, &mix_rewarding)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn add_delegation(
|
||||
&mut self,
|
||||
@@ -639,6 +766,14 @@ pub mod test_helpers {
|
||||
|
||||
let res =
|
||||
try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap();
|
||||
|
||||
if performance.is_zero() {
|
||||
return RewardDistribution {
|
||||
operator: Decimal::zero(),
|
||||
delegates: Decimal::zero(),
|
||||
};
|
||||
}
|
||||
|
||||
let operator: Decimal = find_attribute(
|
||||
Some(MixnetEventType::MixnodeRewarding.to_string()),
|
||||
OPERATOR_REWARD_KEY,
|
||||
@@ -749,6 +884,7 @@ pub mod test_helpers {
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn find_attribute<S: Into<String>>(
|
||||
event_type: Option<S>,
|
||||
attribute: &str,
|
||||
|
||||
@@ -5,10 +5,11 @@ use crate::delegations::storage as delegations_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes,
|
||||
};
|
||||
use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response};
|
||||
use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg;
|
||||
@@ -49,42 +50,159 @@ pub(crate) fn try_migrate_vested_mixnode(
|
||||
Some(&mix_details.bond_information),
|
||||
)?;
|
||||
|
||||
Ok(Response::new().add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: info.sender.into_string(),
|
||||
},
|
||||
vec![],
|
||||
)?))
|
||||
Ok(Response::new()
|
||||
.add_event(Event::new("migrate-vested-mixnode").add_attribute("mix_id", mix_id.to_string()))
|
||||
.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: info.sender.into_string(),
|
||||
},
|
||||
vec![],
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn try_migrate_vested_delegation(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let mut response = Response::new();
|
||||
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
|
||||
|
||||
let storage_key =
|
||||
Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract));
|
||||
let Some(mut delegation) =
|
||||
let Some(vested_delegation) =
|
||||
delegations_storage::delegations().may_load(deps.storage, storage_key.clone())?
|
||||
else {
|
||||
return Err(MixnetContractError::NotAVestingDelegation);
|
||||
};
|
||||
|
||||
// sanity check that's meant to blow up the contract
|
||||
assert_eq!(delegation.proxy, Some(vesting_contract.clone()));
|
||||
assert_eq!(vested_delegation.proxy, Some(vesting_contract.clone()));
|
||||
|
||||
// update the delegation and save it under the correct storage key
|
||||
delegation.proxy = None;
|
||||
let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
|
||||
delegations_storage::delegations().remove(deps.storage, storage_key)?;
|
||||
delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?;
|
||||
let mut updated_delegation = vested_delegation.clone();
|
||||
updated_delegation.proxy = None;
|
||||
|
||||
Ok(Response::new().add_message(wasm_execute(
|
||||
let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
|
||||
|
||||
// remove the old (vested) delegation
|
||||
delegations_storage::delegations().remove(deps.storage, storage_key)?;
|
||||
|
||||
// check if there was already a delegation present under that key (i.e. an old liquid one)
|
||||
if let Some(existing_liquid_delegation) =
|
||||
delegations_storage::delegations().may_load(deps.storage, new_storage_key.clone())?
|
||||
{
|
||||
// treat it as adding extra stake to the existing delegation, so we need to update the unit reward value
|
||||
// as well as retrieve any pending rewards
|
||||
// it replicates part of code from `pending_events::delegate`,
|
||||
// but without some checks that'd be redundant in this instance
|
||||
let mut mix_rewarding =
|
||||
rewards_storage::MIXNODE_REWARDING.load(deps.storage, vested_delegation.mix_id)?;
|
||||
|
||||
// calculate rewards separately for the purposes of emitting those in events
|
||||
let pending_liquid_reward =
|
||||
mix_rewarding.determine_delegation_reward(&existing_liquid_delegation)?;
|
||||
let pending_vested_reward =
|
||||
mix_rewarding.determine_delegation_reward(&vested_delegation)?;
|
||||
|
||||
// the calls to 'undelegate' followed by artificial delegate are performed
|
||||
// to keep the internal `.delegates` field in sync
|
||||
// (this is due to the fact delegation only holds values up in `Uint128` and lacks the precision of a `Decimal`
|
||||
// which has to be used for reward accounting)
|
||||
let liquid_delegation_with_reward =
|
||||
mix_rewarding.undelegate(&existing_liquid_delegation)?;
|
||||
let vested_delegation_with_reward = mix_rewarding.undelegate(&vested_delegation)?;
|
||||
|
||||
// updated delegation amount consists of:
|
||||
// - delegated vested tokens
|
||||
// - delegated liquid tokens
|
||||
// - pending rewards earned by the delegated vested tokens
|
||||
// - pending rewards earned by the delegated liquid tokens
|
||||
let mut updated_total = liquid_delegation_with_reward.clone();
|
||||
updated_total.amount += vested_delegation_with_reward.amount;
|
||||
mix_rewarding.add_base_delegation(updated_total.amount)?;
|
||||
|
||||
updated_delegation.amount = updated_total;
|
||||
updated_delegation.height = env.block.height;
|
||||
updated_delegation.cumulative_reward_ratio = mix_rewarding.total_unit_reward;
|
||||
|
||||
rewards_storage::MIXNODE_REWARDING.save(
|
||||
deps.storage,
|
||||
vested_delegation.mix_id,
|
||||
&mix_rewarding,
|
||||
)?;
|
||||
|
||||
// replace the old delegation with the new one
|
||||
delegations_storage::delegations().replace(
|
||||
deps.storage,
|
||||
new_storage_key,
|
||||
Some(&updated_delegation),
|
||||
Some(&existing_liquid_delegation),
|
||||
)?;
|
||||
|
||||
// just emit EVERYTHING we can. just in case
|
||||
response.events.push(
|
||||
Event::new("migrate-vested-delegation")
|
||||
.add_attribute("mix_id", mix_id.to_string())
|
||||
.add_attribute("existing_liquid", "true")
|
||||
.add_attribute(
|
||||
"old_vested_unit_reward",
|
||||
vested_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_vested_delegation_amount",
|
||||
vested_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_unit_reward",
|
||||
existing_liquid_delegation
|
||||
.cumulative_reward_ratio
|
||||
.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_amount",
|
||||
existing_liquid_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"new_unit_reward",
|
||||
updated_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"new_delegation_amount",
|
||||
updated_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute("applied_liquid_reward", pending_liquid_reward.to_string())
|
||||
.add_attribute("applied_vested_reward", pending_vested_reward.to_string()),
|
||||
)
|
||||
} else {
|
||||
// otherwise, this is as simple as resaving the updated value under the new key
|
||||
delegations_storage::delegations().save(
|
||||
deps.storage,
|
||||
new_storage_key,
|
||||
&updated_delegation,
|
||||
)?;
|
||||
|
||||
response.events.push(
|
||||
Event::new("migrate-vested-delegation")
|
||||
.add_attribute("mix_id", mix_id.to_string())
|
||||
.add_attribute("existing_liquid", "false")
|
||||
.add_attribute(
|
||||
"old_vested_unit_reward",
|
||||
vested_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_vested_delegation_amount",
|
||||
vested_delegation.amount.to_string(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Ok(response.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: info.sender.into_string(),
|
||||
@@ -93,3 +211,472 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
vec![],
|
||||
)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod migrating_vested_mixnode {
|
||||
use super::*;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{from_binary, Addr, CosmosMsg, WasmMsg};
|
||||
|
||||
#[test]
|
||||
fn with_no_bonded_nodes() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
// nothing happens
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap_err();
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked("owner")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_liquid_node_bonded() {
|
||||
let mut test = TestSetup::new();
|
||||
test.add_dummy_mixnode("owner", None);
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
// nothing happens
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingMixnode)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_vested_node_bonded() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode_with_legal_proxy("owner", None);
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
let existing_node = get_mixnode_details_by_id(deps.storage, mix_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(existing_node.bond_information.proxy.is_some());
|
||||
|
||||
let mut expected = existing_node.clone();
|
||||
expected.bond_information.proxy = None;
|
||||
|
||||
// node is simply resaved with proxy data removed and a track message is sent into the vesting contract
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: "owner".to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod migrating_vested_delegation {
|
||||
use super::*;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::mixnodes::storage::mixnode_bonds;
|
||||
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{from_binary, Addr, CosmosMsg, Decimal, Order, Uint128, WasmMsg};
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::reward_params::Performance;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward;
|
||||
use rand::distributions::Distribution;
|
||||
use rand::distributions::WeightedIndex;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::RngCore;
|
||||
|
||||
fn setup_state() -> TestSetup {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let problematic_delegator = "n1unhappydelegator";
|
||||
let problematic_delegator_twin = "n1anotherunhappydelegator";
|
||||
|
||||
let choices = [true, false];
|
||||
|
||||
// every epoch there's a 2% chance of somebody bonding a node
|
||||
let bonding_weights = [2, 98];
|
||||
|
||||
// and 15% of making a delegation
|
||||
let delegation_weights = [15, 85];
|
||||
|
||||
// and 1% of making a VESTED delegation
|
||||
let vested_delegation_weights = [1, 99];
|
||||
|
||||
let bonding_dist = WeightedIndex::new(bonding_weights).unwrap();
|
||||
let delegation_dist = WeightedIndex::new(delegation_weights).unwrap();
|
||||
let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap();
|
||||
|
||||
// make sure we have at least a single node at the beginning
|
||||
let owner = test.random_address();
|
||||
let mix_id = test.add_dummy_mixnode(&owner, None);
|
||||
nodes.push(mix_id);
|
||||
|
||||
// create a bunch of nodes and delegations and progress through epochs
|
||||
for epoch_id in 0..1000 {
|
||||
// go through 1000 epochs
|
||||
|
||||
let owner = test.random_address();
|
||||
let min_stake = 100_000_000;
|
||||
// u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate
|
||||
let variance = test.rng.next_u32();
|
||||
let stake = Uint128::new(min_stake as u128 + variance as u128);
|
||||
|
||||
if choices[bonding_dist.sample(&mut test.rng)] {
|
||||
// bond
|
||||
let mix_id = test.add_dummy_mixnode(&owner, Some(stake));
|
||||
nodes.push(mix_id);
|
||||
}
|
||||
|
||||
if choices[delegation_dist.sample(&mut test.rng)] {
|
||||
// uniformly choose a random node to delegate to
|
||||
let node = nodes.choose(&mut test.rng).unwrap();
|
||||
test.add_immediate_delegation(&owner, stake, *node)
|
||||
}
|
||||
|
||||
if choices[vested_delegation_dist.sample(&mut test.rng)] {
|
||||
// uniformly choose a random node to make vested delegation to
|
||||
let node = nodes.choose(&mut test.rng).unwrap();
|
||||
test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node)
|
||||
}
|
||||
|
||||
// make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node
|
||||
if epoch_id == 123 {
|
||||
test.add_immediate_delegation(problematic_delegator, stake, 4);
|
||||
test.add_immediate_delegation(problematic_delegator_twin, stake, 4);
|
||||
}
|
||||
|
||||
if epoch_id == 666 {
|
||||
test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4);
|
||||
test.add_immediate_delegation_with_legal_proxy(
|
||||
problematic_delegator_twin,
|
||||
stake,
|
||||
4,
|
||||
);
|
||||
}
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
test
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_no_delegation() {
|
||||
let mut test = setup_state();
|
||||
let env = test.env();
|
||||
|
||||
let sender = mock_info("owner-without-any-delegations", &[]);
|
||||
|
||||
// it simply fails for there is nothing to migrate
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_just_liquid_delegation() {
|
||||
let mut test = setup_state();
|
||||
let env = test.env();
|
||||
|
||||
// find a valid delegation
|
||||
let delegation = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_none())
|
||||
.unwrap();
|
||||
|
||||
// make sure we haven't chosen somebody that also has a vested delegation because that would have invalidated the test
|
||||
assert!(!delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.any(|d| d.proxy.is_some() && d.owner.as_str() == delegation.owner.as_str()));
|
||||
|
||||
let sender = mock_info(delegation.owner.as_str(), &[]);
|
||||
let mix_id = delegation.mix_id;
|
||||
|
||||
// it also fails because the method is only allowed for vested delegations
|
||||
let res =
|
||||
try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_just_vested_delegation() {
|
||||
let mut test = setup_state();
|
||||
let env = test.env();
|
||||
|
||||
// find a valid delegation
|
||||
let delegation = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_some())
|
||||
.unwrap();
|
||||
|
||||
// make sure we haven't chosen somebody that also has a liquid delegation because that would have invalidated the test
|
||||
assert!(!delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.any(|d| d.proxy.is_none() && d.owner.as_str() == delegation.owner.as_str()));
|
||||
|
||||
let storage_key = delegation.storage_key();
|
||||
let mut expected_liquid = delegation.clone();
|
||||
expected_liquid.proxy = None;
|
||||
let expected_new_storage_key = expected_liquid.storage_key();
|
||||
|
||||
let sender = mock_info(delegation.owner.as_str(), &[]);
|
||||
let mix_id = delegation.mix_id;
|
||||
|
||||
// a track message is sent into the vesting contract
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: delegation.owner.to_string(),
|
||||
mix_id,
|
||||
}
|
||||
);
|
||||
|
||||
// the entry is gone from the old storage key
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// and is resaved (without proxy) under the new key
|
||||
assert_eq!(
|
||||
expected_liquid,
|
||||
delegations()
|
||||
.load(test.deps().storage, expected_new_storage_key)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_both_liquid_and_vested_delegation() {
|
||||
#[track_caller]
|
||||
fn ensure_delegation_sync(test: &TestSetup, mix_id: MixId) {
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let epsilon = "0.001".parse().unwrap();
|
||||
|
||||
let subtotal: Decimal = delegations()
|
||||
.prefix(mix_id)
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| {
|
||||
d.map(|(_, del)| {
|
||||
let pending_rewards =
|
||||
mix_info.determine_delegation_reward(&del).unwrap();
|
||||
pending_rewards + del.dec_amount().unwrap()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.sum();
|
||||
|
||||
compare_decimals(mix_info.delegates, subtotal, Some(epsilon))
|
||||
}
|
||||
|
||||
let mut test = setup_state();
|
||||
let env = test.env();
|
||||
|
||||
let problematic_delegator = "n1unhappydelegator";
|
||||
let problematic_delegator_twin = "n1anotherunhappydelegator";
|
||||
let mix_id = 4;
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let unclaimed_liquid_reward = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation)
|
||||
.unwrap();
|
||||
let unclaimed_vested_reward = mix_info
|
||||
.determine_delegation_reward(&vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
// sanity check before doing anything
|
||||
ensure_delegation_sync(&test, mix_id);
|
||||
|
||||
// a track message is sent into the vesting contract
|
||||
let sender = mock_info(problematic_delegator, &[]);
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: problematic_delegator.to_string(),
|
||||
mix_id,
|
||||
}
|
||||
);
|
||||
|
||||
let updated_mix_info = test.mix_rewarding(mix_id);
|
||||
assert_eq!(
|
||||
mix_info.unique_delegations - 1,
|
||||
updated_mix_info.unique_delegations
|
||||
);
|
||||
|
||||
// the vested delegation is gone
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, vested_storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let updated_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
assert!(updated_liquid_delegation.proxy.is_none());
|
||||
assert_eq!(
|
||||
updated_liquid_delegation.cumulative_reward_ratio,
|
||||
updated_mix_info.total_unit_reward
|
||||
);
|
||||
|
||||
let expected_amount = truncate_reward(
|
||||
vested_delegation.dec_amount().unwrap()
|
||||
+ liquid_delegation.dec_amount().unwrap()
|
||||
+ unclaimed_liquid_reward
|
||||
+ unclaimed_vested_reward,
|
||||
"unym",
|
||||
);
|
||||
// due to rounding we can expect and tolerate a single token of difference
|
||||
assert_eq_with_leeway(
|
||||
updated_liquid_delegation.amount.amount,
|
||||
expected_amount.amount,
|
||||
Uint128::one(),
|
||||
);
|
||||
|
||||
// this assertion must still hold
|
||||
ensure_delegation_sync(&test, mix_id);
|
||||
|
||||
// go through few more rewarding epochs to make sure the rewards and accounting
|
||||
// would be the same as if the delegations remained separate
|
||||
let all_nodes = mixnode_bonds()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|m| m.map(|(_, node)| node.mix_id).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let twin_liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
let twin_vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
|
||||
let twin_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, twin_liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let twin_vested_delegation = delegations()
|
||||
.load(test.deps().storage, twin_vested_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
let info = test.mix_rewarding(mix_id);
|
||||
|
||||
let unclaimed_rewards_twin_liquid = info
|
||||
.determine_delegation_reward(&twin_liquid_delegation)
|
||||
.unwrap();
|
||||
let unclaimed_rewards_twin_vested = info
|
||||
.determine_delegation_reward(&twin_vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// this assertion must still hold
|
||||
ensure_delegation_sync(&test, mix_id);
|
||||
|
||||
let info = test.mix_rewarding(mix_id);
|
||||
|
||||
let current_liquid = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let rewards = info.determine_delegation_reward(¤t_liquid).unwrap();
|
||||
|
||||
let twin_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, twin_liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let twin_vested_delegation = delegations()
|
||||
.load(test.deps().storage, twin_vested_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
let rewards_twin_liquid = info
|
||||
.determine_delegation_reward(&twin_liquid_delegation)
|
||||
.unwrap();
|
||||
let rewards_twin_vested = info
|
||||
.determine_delegation_reward(&twin_vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
let new_rewards_twin = rewards_twin_liquid + rewards_twin_vested
|
||||
- unclaimed_rewards_twin_liquid
|
||||
- unclaimed_rewards_twin_vested;
|
||||
|
||||
compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user