diff --git a/rs/nns/governance/src/governance.rs b/rs/nns/governance/src/governance.rs index 41e8c09fb468..818842880814 100644 --- a/rs/nns/governance/src/governance.rs +++ b/rs/nns/governance/src/governance.rs @@ -1316,7 +1316,7 @@ impl Governance { cmc: Arc, mut randomness: Box, ) -> Self { - let (heap_governance_proto, maybe_rng_seed) = split_governance_proto(governance_proto); + let (mut heap_governance_proto, maybe_rng_seed) = split_governance_proto(governance_proto); // Carry over the previous rng seed to avoid race conditions in handling queued ingress // messages that may require a functioning RNG. @@ -1324,6 +1324,12 @@ impl Governance { randomness.seed_rng(rng_seed); } + // Migration: reduce neuron_minimum_dissolve_delay_to_vote_seconds to 2 weeks if it is + // currently higher, per the mission70 whitepaper. + Self::maybe_reduce_neuron_minimum_dissolve_delay_to_vote_seconds( + &mut heap_governance_proto, + ); + let mut governance = Self { heap_data: heap_governance_proto, neuron_store: NeuronStore::new_restored(), @@ -4780,9 +4786,61 @@ impl Governance { } pub fn neuron_minimum_dissolve_delay_to_vote_seconds(&self) -> u64 { + let default = if is_mission_70_voting_rewards_enabled() { + 14 * ONE_DAY_SECONDS + } else { + VotingPowerEconomics::DEFAULT_NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS + }; self.voting_power_economics() .neuron_minimum_dissolve_delay_to_vote_seconds - .unwrap_or(VotingPowerEconomics::DEFAULT_NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS) + .unwrap_or(default) + } + + /// Reduces `neuron_minimum_dissolve_delay_to_vote_seconds` to 2 weeks if it is currently + /// higher. This is a one-time migration that takes effect on the first post_upgrade after + /// this code is deployed. + fn maybe_reduce_neuron_minimum_dissolve_delay_to_vote_seconds( + heap_data: &mut HeapGovernanceData, + ) { + if !is_mission_70_voting_rewards_enabled() { + return; + } + + let two_weeks_seconds = 14 * ONE_DAY_SECONDS; + + let Some(voting_power_economics) = heap_data + .economics + .as_mut() + .and_then(|economics| economics.voting_power_economics.as_mut()) + else { + println!( + "{}WARNING: Cannot migrate neuron_minimum_dissolve_delay_to_vote_seconds: \ + voting_power_economics not set.", + LOG_PREFIX, + ); + return; + }; + + let current = voting_power_economics + .neuron_minimum_dissolve_delay_to_vote_seconds + .unwrap_or(VotingPowerEconomics::DEFAULT_NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS); + + assert!( + current >= two_weeks_seconds, + "neuron_minimum_dissolve_delay_to_vote_seconds ({}) is unexpectedly below 2 weeks ({})", + current, + two_weeks_seconds, + ); + + if current > two_weeks_seconds { + println!( + "{}Migrating neuron_minimum_dissolve_delay_to_vote_seconds from {} to {} \ + (2 weeks).", + LOG_PREFIX, current, two_weeks_seconds, + ); + voting_power_economics.neuron_minimum_dissolve_delay_to_vote_seconds = + Some(two_weeks_seconds); + } } /// The proposal id of the next proposal. diff --git a/rs/nns/governance/src/governance/tests/mod.rs b/rs/nns/governance/src/governance/tests/mod.rs index fd1425946194..ff94d34a4b7f 100644 --- a/rs/nns/governance/src/governance/tests/mod.rs +++ b/rs/nns/governance/src/governance/tests/mod.rs @@ -8,7 +8,9 @@ use crate::{ test_utils::{MockEnvironment, StubCMC, StubIcpLedger}, }; use ic_base_types::PrincipalId; -use ic_nervous_system_common::{E8, assert_is_err, assert_is_ok}; +use ic_nervous_system_common::{ + E8, ONE_DAY_SECONDS, ONE_MONTH_SECONDS, assert_is_err, assert_is_ok, +}; #[cfg(feature = "test")] use ic_nervous_system_proto::pb::v1::GlobalTimeOfDay; use ic_nns_common::pb::v1::NeuronId; @@ -1799,3 +1801,54 @@ fn test_maybe_set_eight_year_gang_bonus_base() { .unwrap(); assert_eq!(bonus, 140 * E8); } + +#[test] +fn test_post_upgrade_migrates_neuron_minimum_dissolve_delay_to_vote_seconds() { + let _mission70 = crate::temporarily_enable_mission_70_voting_rewards(); + let two_weeks_seconds = 14 * ONE_DAY_SECONDS; + + // Simulate the old production state: 6 months. + let mut heap_data = HeapGovernanceData { + economics: Some(NetworkEconomics { + voting_power_economics: Some(VotingPowerEconomics { + neuron_minimum_dissolve_delay_to_vote_seconds: Some(6 * ONE_MONTH_SECONDS), + ..VotingPowerEconomics::with_default_values() + }), + ..NetworkEconomics::with_default_values() + }), + ..Default::default() + }; + + Governance::maybe_reduce_neuron_minimum_dissolve_delay_to_vote_seconds(&mut heap_data); + + let actual = heap_data + .economics + .as_ref() + .unwrap() + .voting_power_economics + .as_ref() + .unwrap() + .neuron_minimum_dissolve_delay_to_vote_seconds; + assert_eq!(actual, Some(two_weeks_seconds)); +} + +#[test] +#[should_panic(expected = "unexpectedly below 2 weeks")] +fn test_post_upgrade_panics_if_neuron_minimum_dissolve_delay_to_vote_seconds_below_two_weeks() { + let _mission70 = crate::temporarily_enable_mission_70_voting_rewards(); + let one_week_seconds = 7 * ONE_DAY_SECONDS; + + let mut heap_data = HeapGovernanceData { + economics: Some(NetworkEconomics { + voting_power_economics: Some(VotingPowerEconomics { + neuron_minimum_dissolve_delay_to_vote_seconds: Some(one_week_seconds), + ..VotingPowerEconomics::with_default_values() + }), + ..NetworkEconomics::with_default_values() + }), + ..Default::default() + }; + + // Should panic because the value is below 2 weeks. + Governance::maybe_reduce_neuron_minimum_dissolve_delay_to_vote_seconds(&mut heap_data); +} diff --git a/rs/nns/governance/src/network_economics.rs b/rs/nns/governance/src/network_economics.rs index 4d7a1e697c2d..eafe1a9aaf52 100644 --- a/rs/nns/governance/src/network_economics.rs +++ b/rs/nns/governance/src/network_economics.rs @@ -285,7 +285,7 @@ impl VotingPowerEconomics { /// which originate from the time when the minimum dissolve delay to vote was an internal NNS /// constant. pub const NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS_BOUNDS: RangeInclusive = - (3 * ONE_MONTH_SECONDS)..=(6 * ONE_MONTH_SECONDS); + (14 * ONE_DAY_SECONDS)..=(6 * ONE_MONTH_SECONDS); pub const DEFAULT_START_REDUCING_VOTING_POWER_AFTER_SECONDS: u64 = 6 * ONE_MONTH_SECONDS; @@ -369,8 +369,8 @@ impl VotingPowerEconomics { .contains(&delay) { let defect = format!( - "neuron_minimum_dissolve_delay_to_vote_seconds ({:?}) must be between three \ - and six months.", + "neuron_minimum_dissolve_delay_to_vote_seconds ({:?}) must be between two \ + weeks and six months.", self.neuron_minimum_dissolve_delay_to_vote_seconds ); defects.push(defect); diff --git a/rs/nns/governance/src/network_economics_tests.rs b/rs/nns/governance/src/network_economics_tests.rs index b35ceb7e6410..802d0e2f1678 100644 --- a/rs/nns/governance/src/network_economics_tests.rs +++ b/rs/nns/governance/src/network_economics_tests.rs @@ -91,9 +91,9 @@ fn test_network_economics_with_default_values_is_valid() { #[test] fn test_neuron_minimum_dissolve_delay_to_vote_seconds_bounds() { // Define constants for better readability and maintainability - const LOWER_BOUND_SECONDS: u64 = 3 * ONE_MONTH_SECONDS; + const LOWER_BOUND_SECONDS: u64 = 14 * ONE_DAY_SECONDS; const UPPER_BOUND_SECONDS: u64 = 6 * ONE_MONTH_SECONDS; - const DEFAULT_SECONDS: u64 = LOWER_BOUND_SECONDS; // Assuming default is the minimum + const DEFAULT_SECONDS: u64 = UPPER_BOUND_SECONDS; // Test cases: (delay in seconds, expected result) let test_cases = [ @@ -106,14 +106,14 @@ fn test_neuron_minimum_dissolve_delay_to_vote_seconds_bounds() { ( Some(LOWER_BOUND_SECONDS - 1), Err(vec![format!( - "neuron_minimum_dissolve_delay_to_vote_seconds (Some({})) must be between three and six months.", + "neuron_minimum_dissolve_delay_to_vote_seconds (Some({})) must be between two weeks and six months.", LOWER_BOUND_SECONDS - 1 )]), ), ( Some(UPPER_BOUND_SECONDS + 1), Err(vec![format!( - "neuron_minimum_dissolve_delay_to_vote_seconds (Some({})) must be between three and six months.", + "neuron_minimum_dissolve_delay_to_vote_seconds (Some({})) must be between two weeks and six months.", UPPER_BOUND_SECONDS + 1 )]), ), diff --git a/rs/nns/governance/src/neuron_store/voting_power.rs b/rs/nns/governance/src/neuron_store/voting_power.rs index b2dcc8900a28..e0d66fe064bc 100644 --- a/rs/nns/governance/src/neuron_store/voting_power.rs +++ b/rs/nns/governance/src/neuron_store/voting_power.rs @@ -3,6 +3,7 @@ use ic_nns_governance_api::Vote; use super::{NeuronStore, NeuronStoreError}; use crate::{ + is_mission_70_voting_rewards_enabled, neuron::Neuron, pb::v1::{Ballot, NeuronIdToVotingPowerMap, VotingPowerEconomics, VotingPowerTotal}, storage::neurons::NeuronSections, @@ -131,9 +132,14 @@ impl NeuronStore { let mut total_deciding_voting_power: u128 = 0; let mut total_potential_voting_power: u128 = 0; + let default_min_dissolve_delay = if is_mission_70_voting_rewards_enabled() { + 14 * ic_nervous_system_common::ONE_DAY_SECONDS + } else { + VotingPowerEconomics::DEFAULT_NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS + }; let min_dissolve_delay_seconds = voting_power_economics .neuron_minimum_dissolve_delay_to_vote_seconds - .unwrap_or(VotingPowerEconomics::DEFAULT_NEURON_MINIMUM_DISSOLVE_DELAY_TO_VOTE_SECONDS); + .unwrap_or(default_min_dissolve_delay); let mut process_neuron = |neuron: &Neuron| { if neuron.is_inactive(now_seconds)