Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f16704a0c5 | |||
| 66e4021530 | |||
| 1f089ca6ad | |||
| ae30b0d803 | |||
| 2cdf2ec0c9 | |||
| 2eb1862eaa | |||
| 4383aa84b8 |
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: arc-ubuntu-20.04
|
||||
platform: [ arc-ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
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 {
|
||||
assert!(a - b < epsilon, "{a} != {b}")
|
||||
assert!(a - b < epsilon, "{a} != {b}, delta: {}", a - b)
|
||||
} else {
|
||||
assert!(b - a < epsilon, "{a} != {b}")
|
||||
assert!(b - a < epsilon, "{a} != {b}, delta: {}", b - a)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -532,7 +532,7 @@ pub enum QueryMsg {
|
||||
/// Gets the basic list of all unbonded mixnodes that belonged to a particular owner.
|
||||
#[cfg_attr(feature = "schema", returns(PagedUnbondedMixnodesResponse))]
|
||||
GetUnbondedMixNodesByOwner {
|
||||
/// The address of the owner of the the mixnodes used for the query.
|
||||
/// The address of the owner of the mixnodes used for the query.
|
||||
owner: String,
|
||||
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
@@ -783,7 +783,26 @@ pub enum QueryMsg {
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AffectedDelegator {
|
||||
pub address: String,
|
||||
pub missing_ratio: Decimal,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AffectedNode {
|
||||
pub mix_id: MixId,
|
||||
pub delegators: Vec<AffectedDelegator>,
|
||||
}
|
||||
|
||||
impl AffectedNode {
|
||||
pub fn total_ratio(&self) -> Decimal {
|
||||
self.delegators.iter().map(|d| d.missing_ratio).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
pub fix_nodes: Option<Vec<AffectedNode>>,
|
||||
}
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
@@ -308,6 +308,7 @@ pub fn instantiate_contracts(
|
||||
mixnet_contract_address.clone(),
|
||||
&nym_mixnet_contract_common::MigrateMsg {
|
||||
vesting_contract_address: Some(vesting_contract_address.to_string()),
|
||||
fix_nodes: None,
|
||||
},
|
||||
mixnet_code_id,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -2150,7 +2150,7 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"description": "The address of the owner of the the mixnodes used for the query.",
|
||||
"description": "The address of the owner of the mixnodes used for the query.",
|
||||
"type": "string"
|
||||
},
|
||||
"start_after": {
|
||||
@@ -2961,6 +2961,15 @@
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fix_nodes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedNode"
|
||||
}
|
||||
},
|
||||
"vesting_contract_address": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -2968,7 +2977,50 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"AffectedDelegator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"missing_ratio"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"missing_ratio": {
|
||||
"$ref": "#/definitions/Decimal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AffectedNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"delegators",
|
||||
"mix_id"
|
||||
],
|
||||
"properties": {
|
||||
"delegators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedDelegator"
|
||||
}
|
||||
},
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Decimal": {
|
||||
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fix_nodes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedNode"
|
||||
}
|
||||
},
|
||||
"vesting_contract_address": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -10,5 +19,48 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"AffectedDelegator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"missing_ratio"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"missing_ratio": {
|
||||
"$ref": "#/definitions/Decimal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AffectedNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"delegators",
|
||||
"mix_id"
|
||||
],
|
||||
"properties": {
|
||||
"delegators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedDelegator"
|
||||
}
|
||||
},
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Decimal": {
|
||||
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"description": "The address of the owner of the the mixnodes used for the query.",
|
||||
"description": "The address of the owner of the mixnodes used for the query.",
|
||||
"type": "string"
|
||||
},
|
||||
"start_after": {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMO
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnode_storage;
|
||||
use crate::queued_migrations;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response,
|
||||
@@ -272,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
|
||||
@@ -539,7 +540,7 @@ pub fn query(
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
env: Env,
|
||||
msg: MigrateMsg,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
@@ -555,7 +556,13 @@ pub fn migrate(
|
||||
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, ¤t_state)?;
|
||||
}
|
||||
|
||||
Ok(Default::default())
|
||||
let mut response = Response::new();
|
||||
|
||||
if let Some(nodes_to_fix) = msg.fix_nodes {
|
||||
queued_migrations::restore_vested_delegations(&mut response, deps, env, nodes_to_fix)?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,2 +1,669 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::rewards::storage::MIXNODE_REWARDING;
|
||||
use cosmwasm_std::{Addr, Decimal, DepsMut, Env, Event, Order, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::helpers::IntoBaseDecimal;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward;
|
||||
use mixnet_contract_common::{AffectedNode, Delegation};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn fix_affected_node(
|
||||
response: &mut Response,
|
||||
deps: DepsMut<'_>,
|
||||
env: &Env,
|
||||
node: AffectedNode,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let total_ratio = node.total_ratio();
|
||||
let one = Decimal::one();
|
||||
|
||||
// the total ratio has to be equal to 1 (or be extremely close to it, because it can be affected by rounding)
|
||||
// if it doesn't it means we passed an invalid migrate msg and we HAVE TO fail the migration if that's the case
|
||||
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
|
||||
|
||||
if total_ratio > one {
|
||||
if total_ratio - one >= epsilon {
|
||||
return Err(MixnetContractError::FailedMigration {
|
||||
comment: format!(
|
||||
"the total delegation ratio for node {} does not sum up to 1",
|
||||
node.mix_id
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if one - total_ratio >= epsilon {
|
||||
return Err(MixnetContractError::FailedMigration {
|
||||
comment: format!(
|
||||
"the total delegation ratio for node {} does not sum up to 1",
|
||||
node.mix_id
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let mut total_accounted_for = Decimal::zero();
|
||||
let mut mix_rewarding = MIXNODE_REWARDING.load(deps.storage, node.mix_id)?;
|
||||
|
||||
let mut cached_delegations = BTreeMap::new();
|
||||
|
||||
// determine all the stake accounted for, i.e. all delegations and their pending rewards
|
||||
for entry in delegations()
|
||||
.idx
|
||||
.mixnode
|
||||
.prefix(node.mix_id)
|
||||
.range(deps.storage, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
{
|
||||
let delegation = entry?;
|
||||
let base_delegation = delegation.dec_amount()?;
|
||||
let pending_reward = mix_rewarding.determine_delegation_reward(&delegation)?;
|
||||
|
||||
// cache the delegation and reward for the lookup in the next loop
|
||||
if node
|
||||
.delegators
|
||||
.iter()
|
||||
.any(|d| d.address == delegation.owner.as_str())
|
||||
{
|
||||
cached_delegations.insert(delegation.owner.to_string(), (delegation, pending_reward));
|
||||
}
|
||||
|
||||
total_accounted_for += base_delegation;
|
||||
total_accounted_for += pending_reward;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
assert!(cached_delegations.len() <= node.delegators.len());
|
||||
|
||||
// the missing stake equals to the difference between total node delegation (which includes all rewards, etc.)
|
||||
// and the value we managed to just account for
|
||||
let node_missing = mix_rewarding.delegates - total_accounted_for;
|
||||
|
||||
let mut distributed = Decimal::zero();
|
||||
|
||||
// finally split the missing stake among the affected delegators according to the ratios
|
||||
// provided in the migration which were very painstakingly determined by scraping different
|
||||
// sources of chain data
|
||||
for delegator in node.delegators {
|
||||
let restored = node_missing * delegator.missing_ratio;
|
||||
distributed += restored;
|
||||
|
||||
// we have two scenarios to cover here:
|
||||
// 1. somebody performed vested migration and then undelegated the tokens (*sigh*)
|
||||
// - in that case we have to create brand-new delegation with the restored amount
|
||||
// 2. the delegation still exists
|
||||
// - in that case we have to increase the existing delegation. essentially treat it as if somebody delegated extra tokens
|
||||
|
||||
if let Some((old_liquid_delegation, pending_reward)) =
|
||||
cached_delegations.remove(&delegator.address)
|
||||
{
|
||||
// delegation still exists
|
||||
|
||||
assert!(old_liquid_delegation.proxy.is_none());
|
||||
|
||||
let old_liquid = old_liquid_delegation.dec_amount()? + pending_reward;
|
||||
let updated_amount_dec = old_liquid + restored;
|
||||
let updated_amount =
|
||||
truncate_reward(updated_amount_dec, &old_liquid_delegation.amount.denom);
|
||||
|
||||
// take the truncation into consideration for the purposes of future accounting
|
||||
let truncated_delta = updated_amount_dec - updated_amount.amount.into_base_decimal()?;
|
||||
mix_rewarding.delegates -= truncated_delta;
|
||||
|
||||
// just emit EVERYTHING we can. just in case
|
||||
response.events.push(
|
||||
Event::new("delegation_restoration")
|
||||
.add_attribute("delegator", delegator.address)
|
||||
.add_attribute("delegator_ratio", delegator.missing_ratio.to_string())
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("restored_amount_dec", restored.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("updated_amount_dec", updated_amount_dec.to_string())
|
||||
.add_attribute("updated_amount", updated_amount.to_string())
|
||||
.add_attribute("liquid_delegation_existed", "true")
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_unit_reward",
|
||||
old_liquid_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_amount",
|
||||
old_liquid_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_pending_reward",
|
||||
pending_reward.to_string(),
|
||||
)
|
||||
.add_attribute("truncated_amount", truncated_delta.to_string()),
|
||||
);
|
||||
|
||||
// create new delegation with the updated amount
|
||||
// and also, what's very important, with correct unit reward amount
|
||||
let updated_delegation = Delegation::new(
|
||||
old_liquid_delegation.owner.clone(),
|
||||
node.mix_id,
|
||||
mix_rewarding.total_unit_reward,
|
||||
updated_amount,
|
||||
env.block.height,
|
||||
);
|
||||
|
||||
// replace the value stored under the existing key
|
||||
let delegation_storage_key = old_liquid_delegation.storage_key();
|
||||
delegations().replace(
|
||||
deps.storage,
|
||||
delegation_storage_key,
|
||||
Some(&updated_delegation),
|
||||
Some(&old_liquid_delegation),
|
||||
)?;
|
||||
} else {
|
||||
let restored_amount = truncate_reward(restored, "unym");
|
||||
|
||||
// take the truncation into consideration for the purposes of future accounting
|
||||
let truncated_delta = restored - restored_amount.amount.into_base_decimal()?;
|
||||
mix_rewarding.delegates -= truncated_delta;
|
||||
|
||||
// delegation is now gone - create a new one with the restored amount
|
||||
let delegation = Delegation::new(
|
||||
Addr::unchecked(&delegator.address),
|
||||
node.mix_id,
|
||||
mix_rewarding.total_unit_reward,
|
||||
restored_amount,
|
||||
env.block.height,
|
||||
);
|
||||
|
||||
let delegation_storage_key = delegation.storage_key();
|
||||
delegations().save(deps.storage, delegation_storage_key, &delegation)?;
|
||||
|
||||
response.events.push(
|
||||
Event::new("delegation_restoration")
|
||||
.add_attribute("delegator", delegator.address)
|
||||
.add_attribute("delegator_ratio", delegator.missing_ratio.to_string())
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("restored_amount_dec", restored.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("updated_amount_dec", restored.to_string())
|
||||
.add_attribute("updated_amount", delegation.amount.to_string())
|
||||
.add_attribute("liquid_delegation_existed", "false")
|
||||
.add_attribute("truncated_amount", truncated_delta.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// the vested and liquid delegations got combined into one
|
||||
mix_rewarding.unique_delegations -= 1;
|
||||
MIXNODE_REWARDING.save(deps.storage, node.mix_id, &mix_rewarding)?;
|
||||
}
|
||||
|
||||
response.events.push(
|
||||
Event::new("node_delegation_restoration")
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("total_redistributed", distributed.to_string()),
|
||||
);
|
||||
|
||||
// another sanity check
|
||||
assert!(distributed <= node_missing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_vested_delegations(
|
||||
response: &mut Response,
|
||||
mut deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
affected_nodes: Vec<AffectedNode>,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
for node in affected_nodes {
|
||||
fix_affected_node(response, deps.branch(), &env, node)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod restoring_vested_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use crate::vesting_migration::try_migrate_vested_delegation;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::Uint128;
|
||||
use mixnet_contract_common::reward_params::Performance;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
|
||||
use mixnet_contract_common::AffectedDelegator;
|
||||
use nym_contracts_common::truncate_decimal;
|
||||
use rand::RngCore;
|
||||
|
||||
#[test]
|
||||
fn for_node_with_single_affected_delegator_without_undelegating() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let mix_id = 4;
|
||||
|
||||
// "accidentally" overwrite the delegation
|
||||
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 vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mut bad_liquid_delegation = vested_delegation.clone();
|
||||
bad_liquid_delegation.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key,
|
||||
&bad_liquid_delegation,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
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();
|
||||
}
|
||||
|
||||
// restoring problematic delegator should be equivalent to the delegator twin just migrating
|
||||
let env = test.env();
|
||||
fix_affected_node(
|
||||
&mut Response::new(),
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: Decimal::one(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
try_migrate_vested_delegation(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(problematic_delegator_twin, &[]),
|
||||
mix_id,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let liquid_storage_key_twin = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key_twin)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
liquid_delegation.cumulative_reward_ratio,
|
||||
liquid_delegation_alt.cumulative_reward_ratio
|
||||
);
|
||||
assert_eq_with_leeway(
|
||||
liquid_delegation.amount.amount,
|
||||
liquid_delegation_alt.amount.amount,
|
||||
Uint128::one(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_node_with_single_affected_delegator_after_undelegating() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let mix_id = 4;
|
||||
|
||||
// "accidentally" overwrite the delegation
|
||||
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 vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mut bad_liquid_delegation = vested_delegation.clone();
|
||||
bad_liquid_delegation.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key,
|
||||
&bad_liquid_delegation,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
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();
|
||||
}
|
||||
|
||||
// they got scared and undelegated (the removed part is their vested delegation)
|
||||
test.remove_immediate_delegation(problematic_delegator, mix_id);
|
||||
|
||||
// go through some more rewarding
|
||||
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();
|
||||
}
|
||||
|
||||
// the restored amount should be equivalent to the liquid part (+ rewards) of the twin delegator
|
||||
let env = test.env();
|
||||
fix_affected_node(
|
||||
&mut Response::new(),
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: Decimal::one(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let liquid_storage_key_twin = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key_twin)
|
||||
.unwrap();
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let pending_twin_reward = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_alt)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
liquid_delegation.cumulative_reward_ratio,
|
||||
mix_info.total_unit_reward
|
||||
);
|
||||
assert_eq_with_leeway(
|
||||
liquid_delegation.amount.amount,
|
||||
liquid_delegation_alt.amount.amount + truncate_reward_amount(pending_twin_reward),
|
||||
Uint128::one(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_node_with_multiple_affected_delegators() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
// some random delegator
|
||||
let problematic_delegator = "n1foomp";
|
||||
|
||||
// another delegator that made DIFFERENT delegations as the previous ones BUT to the same node
|
||||
let problematic_delegator_alt_twin = "n1whatever";
|
||||
|
||||
let mix_id = 4;
|
||||
let mix_info_start = test.mix_rewarding(mix_id);
|
||||
|
||||
// "accidentally" overwrite the delegations
|
||||
let liquid_storage_key1 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key1 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let liquid_delegation1 = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1.clone())
|
||||
.unwrap();
|
||||
let vested_delegation1 = delegations()
|
||||
.load(test.deps().storage, vested_storage_key1.clone())
|
||||
.unwrap();
|
||||
|
||||
// keep track of the 'lost' tokens for test assertions
|
||||
let lost1 = liquid_delegation1.dec_amount().unwrap()
|
||||
+ mix_info_start
|
||||
.determine_delegation_reward(&liquid_delegation1)
|
||||
.unwrap();
|
||||
|
||||
let mut bad_liquid_delegation1 = vested_delegation1.clone();
|
||||
bad_liquid_delegation1.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key1)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key1.clone(),
|
||||
&bad_liquid_delegation1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key2 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_alt_twin),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key2 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_alt_twin),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let liquid_delegation2 = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2.clone())
|
||||
.unwrap();
|
||||
let vested_delegation2 = delegations()
|
||||
.load(test.deps().storage, vested_storage_key2.clone())
|
||||
.unwrap();
|
||||
let lost2 = liquid_delegation2.dec_amount().unwrap()
|
||||
+ mix_info_start
|
||||
.determine_delegation_reward(&liquid_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let mut bad_liquid_delegation2 = vested_delegation2.clone();
|
||||
bad_liquid_delegation2.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key2)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key2.clone(),
|
||||
&bad_liquid_delegation2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// those ratios got determined externally. in this test we unfortunately use purely artificial values
|
||||
let ratio1: Decimal = "0.45326524362".parse().unwrap();
|
||||
let ratio2 = Decimal::one() - ratio1;
|
||||
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let liquid_delegation_before = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1.clone())
|
||||
.unwrap();
|
||||
let liquid_reward_before = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_before)
|
||||
.unwrap();
|
||||
|
||||
let liquid_delegation_alt_before = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2.clone())
|
||||
.unwrap();
|
||||
let liquid_reward_alt_before = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_alt_before)
|
||||
.unwrap();
|
||||
|
||||
let env = test.env();
|
||||
let mut res = Response::new();
|
||||
fix_affected_node(
|
||||
&mut res,
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![
|
||||
AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: ratio1,
|
||||
},
|
||||
AffectedDelegator {
|
||||
address: problematic_delegator_alt_twin.to_string(),
|
||||
missing_ratio: ratio2,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2)
|
||||
.unwrap();
|
||||
|
||||
// the total amount recovered must be equal to what has been lost (approximately)
|
||||
let total_lost = lost1 + lost2;
|
||||
// determine the compounded rewards on the lost tokens
|
||||
// (just unroll `MixNodeRewarding::determine_delegation_reward(...)`)
|
||||
let starting_ratio = mix_info_start.total_unit_reward;
|
||||
let ending_ratio = mix_info.full_reward_ratio();
|
||||
let adjust = starting_ratio + mix_info.unit_delegation;
|
||||
let compounded_lost_reward = (ending_ratio - starting_ratio) * total_lost / adjust;
|
||||
|
||||
let before = liquid_delegation_before.dec_amount().unwrap()
|
||||
+ liquid_delegation_alt_before.dec_amount().unwrap()
|
||||
+ liquid_reward_before
|
||||
+ liquid_reward_alt_before;
|
||||
|
||||
let after = liquid_delegation.amount.amount + liquid_delegation_alt.amount.amount;
|
||||
let expected_before = truncate_decimal(total_lost + compounded_lost_reward + before);
|
||||
|
||||
assert_eq_with_leeway(after, expected_before, Uint128::one());
|
||||
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
|
||||
// more rewarding
|
||||
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();
|
||||
}
|
||||
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod test_helpers {
|
||||
use crate::contract::instantiate;
|
||||
use crate::delegations::queries::query_mixnode_delegations_paged;
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::delegations::transactions::try_delegate_to_mixnode;
|
||||
use crate::families::transactions::{try_create_family, try_join_family};
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
@@ -25,7 +26,7 @@ pub mod test_helpers {
|
||||
rewarding_validator_address,
|
||||
};
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::mixnodes::storage::mixnode_bonds;
|
||||
use crate::mixnodes::storage::{assign_layer, mixnode_bonds, next_mixnode_id_counter};
|
||||
use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode};
|
||||
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};
|
||||
@@ -52,6 +53,7 @@ pub mod test_helpers {
|
||||
may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
};
|
||||
use mixnet_contract_common::families::FamilyHead;
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
@@ -69,19 +71,23 @@ pub mod test_helpers {
|
||||
};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::identity::KeyPair;
|
||||
use rand::distributions::WeightedIndex;
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) {
|
||||
if a > b {
|
||||
assert!(a - b <= leeway)
|
||||
assert!(a - b <= leeway, "{} != {}", a, b)
|
||||
} else {
|
||||
assert!(b - a <= leeway)
|
||||
assert!(b - a <= leeway, "{} != {}", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_decimals(a: Decimal, b: Decimal) {
|
||||
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
|
||||
if a > b {
|
||||
@@ -120,6 +126,133 @@ pub mod test_helpers {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_complex() -> Self {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let problematic_delegator_alt_twin = "n1whatever";
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
if epoch_id == 234 {
|
||||
test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4);
|
||||
}
|
||||
|
||||
if epoch_id == 420 {
|
||||
test.add_immediate_delegation_with_legal_proxy(
|
||||
problematic_delegator_alt_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
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn ensure_delegation_sync(&self, mix_id: MixId) {
|
||||
let mix_info = self.mix_rewarding(mix_id);
|
||||
let epsilon = "0.001".parse().unwrap();
|
||||
|
||||
let subtotal: Decimal = delegations()
|
||||
.prefix(mix_id)
|
||||
.range(self.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))
|
||||
}
|
||||
|
||||
pub fn random_address(&mut self) -> String {
|
||||
format!("n1foomp{}", self.rng.next_u64())
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.deps.as_ref()
|
||||
}
|
||||
@@ -146,6 +279,20 @@ 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 all_mixnodes(&self) -> Vec<MixId> {
|
||||
mixnode_bonds()
|
||||
.range(self.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|m| m.map(|(_, node)| node.mix_id).ok())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, rewarding_denom(self.deps().storage).unwrap())
|
||||
}
|
||||
@@ -266,6 +413,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 +667,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 +901,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 +1019,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,354 @@ 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::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{from_binary, Addr, CosmosMsg, 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::RngCore;
|
||||
|
||||
#[test]
|
||||
fn with_no_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
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 = TestSetup::new_complex();
|
||||
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 = TestSetup::new_complex();
|
||||
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() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
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
|
||||
test.ensure_delegation_sync(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
|
||||
test.ensure_delegation_sync(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 = test.all_mixnodes();
|
||||
|
||||
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
|
||||
test.ensure_delegation_sync(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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ impl NetworkManager {
|
||||
) -> Result<nym_mixnet_contract_common::MigrateMsg, NetworkManagerError> {
|
||||
Ok(nym_mixnet_contract_common::MigrateMsg {
|
||||
vesting_contract_address: Some(ctx.network.contracts.vesting.address()?.to_string()),
|
||||
fix_nodes: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user