From 2eb1862eaa467972863d213b31189c54c3aae6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 9 Oct 2024 09:39:44 +0100 Subject: [PATCH] account for operators who have undelegated since --- contracts/mixnet/src/queued_migrations.rs | 117 +++++++++++++++++----- 1 file changed, 90 insertions(+), 27 deletions(-) diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index f097f9457f..661e6ffefd 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -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(())