added unit tests for vested migrations and fixed additional issues

This commit is contained in:
Jędrzej Stuczyński
2024-10-09 11:46:36 +01:00
parent 2eb1862eaa
commit 2cdf2ec0c9
6 changed files with 748 additions and 21 deletions
@@ -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 {
+4 -2
View File
@@ -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",
]
+1
View File
@@ -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]
+1 -1
View File
@@ -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
+139 -3
View File
@@ -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,
+602 -15
View File
@@ -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(&current_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()))
}
}
}