restoring lost delegations

This commit is contained in:
Jędrzej Stuczyński
2024-10-08 17:00:28 +01:00
parent 27ac34522c
commit 4383aa84b8
3 changed files with 171 additions and 3 deletions
@@ -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: Vec<AffectedNode>,
}
+7 -2
View File
@@ -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,
@@ -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,11 @@ pub fn migrate(
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &current_state)?;
}
Ok(Default::default())
let mut response = Response::new();
queued_migrations::restore_vested_delegations(&mut response, deps, env, msg.fix_nodes)?;
Ok(response)
}
#[cfg(test)]
+144
View File
@@ -1,2 +1,146 @@
// 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::{Decimal, DepsMut, Env, Event, Order, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::rewarding::helpers::truncate_reward;
use mixnet_contract_common::{AffectedNode, Delegation};
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)
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)?;
// keep cache of entries we're interested in because we'll have to update them
// (keep it in a vec since the hashmap overhead is actually larger for the few entries we're dealing with)
let mut cached_delegations = Vec::new();
// determine the total missing stake
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)?;
if node
.delegators
.iter()
.any(|d| d.address == delegation.owner.as_str())
{
cached_delegations.push((delegation, pending_reward));
}
total_accounted_for += base_delegation;
total_accounted_for += pending_reward;
}
// sanity check assertion (we WANT TO blow up if it doesn't hold)
assert_eq!(cached_delegations.len(), node.delegators.len());
let node_missing = mix_rewarding.delegates - total_accounted_for;
let mut distributed = Decimal::zero();
// finally split the missing stake among the affected delegators
for delegator in node.delegators {
// I really hope the affected people haven't attempted undelegating their tokens...
#[allow(clippy::unwrap_used)]
let (delegation, pending_reward) = cached_delegations
.iter()
.find(|d| d.0.owner.as_str() == delegator.address)
.unwrap();
assert!(delegation.proxy.is_none());
let old_liquid = delegation.dec_amount()? + pending_reward;
let restored = node_missing * delegator.missing_ratio;
let updated_amount_dec = old_liquid + restored;
let updated_amount = truncate_reward(updated_amount_dec, &delegation.amount.denom);
distributed += restored;
// 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("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("restored_amount_dec", updated_amount_dec.to_string())
.add_attribute("restored_amount", updated_amount.to_string()),
);
let updated_delegation = Delegation::new(
delegation.owner.clone(),
node.mix_id,
mix_rewarding.total_unit_reward,
updated_amount,
env.block.height,
);
let delegation_storage_key = delegation.storage_key();
delegations().replace(
deps.storage,
delegation_storage_key,
Some(&updated_delegation),
Some(delegation),
)?;
// the vested and liquid delegations got combined into one
mix_rewarding.unique_delegations -= 1;
MIXNODE_REWARDING.save(deps.storage, node.mix_id, &mix_rewarding)?;
}
// 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(())
}