account for operators who have undelegated since

This commit is contained in:
Jędrzej Stuczyński
2024-10-09 09:39:44 +01:00
parent 4383aa84b8
commit 2eb1862eaa
+90 -27
View File
@@ -3,10 +3,11 @@
use crate::delegations::storage::delegations;
use crate::rewards::storage::MIXNODE_REWARDING;
use cosmwasm_std::{Decimal, DepsMut, Env, Event, Order, Response};
use cosmwasm_std::{Addr, 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};
use std::collections::BTreeMap;
fn fix_affected_node(
response: &mut Response,
@@ -18,6 +19,7 @@ fn fix_affected_node(
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 {
@@ -41,11 +43,9 @@ fn fix_affected_node(
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();
let mut cached_delegations = BTreeMap::new();
// determine the total missing stake
// determine all the stake accounted for, i.e. all delegations and their pending rewards
for entry in delegations()
.idx
.mixnode
@@ -57,77 +57,140 @@ fn fix_affected_node(
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.push((delegation, pending_reward));
cached_delegations.insert(delegation.owner.to_string(), (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());
// 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
// 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 {
// 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;
// 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);
// 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("restored_amount_dec", updated_amount_dec.to_string())
.add_attribute("restored_amount", updated_amount.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(),
),
);
// create new delegation with the updated amount
// and also, what's very important, with correct unit reward amount
let updated_delegation = Delegation::new(
delegation.owner.clone(),
old_liquid_delegation.owner.clone(),
node.mix_id,
mix_rewarding.total_unit_reward,
updated_amount,
env.block.height,
);
let delegation_storage_key = delegation.storage_key();
// 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(delegation),
Some(&old_liquid_delegation),
)?;
} else {
// 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,
truncate_reward(restored, "unym"),
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"),
);
}
// 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(())