diff --git a/rs/nervous_system/integration_tests/src/create_service_nervous_system_builder.rs b/rs/nervous_system/integration_tests/src/create_service_nervous_system_builder.rs index 92a3232bd0b..f95d4b33266 100644 --- a/rs/nervous_system/integration_tests/src/create_service_nervous_system_builder.rs +++ b/rs/nervous_system/integration_tests/src/create_service_nervous_system_builder.rs @@ -122,6 +122,12 @@ impl CreateServiceNervousSystemBuilder { self } + pub fn with_minimum_participants(mut self, minimum_participants: u64) -> Self { + let swap_parameters = self.0.swap_parameters.as_mut().unwrap(); + swap_parameters.minimum_participants = Some(minimum_participants); + self + } + pub fn build(self) -> CreateServiceNervousSystem { self.0 } diff --git a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs index b0dd0ec1057..3720badc281 100644 --- a/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs +++ b/rs/nervous_system/integration_tests/src/pocket_ic_helpers.rs @@ -1998,7 +1998,10 @@ pub mod sns { pub mod root { use super::*; - use ic_sns_root::pb::v1::ListSnsCanistersRequest; + use ic_sns_root::{ + pb::v1::ListSnsCanistersRequest, GetSnsCanistersSummaryRequest, + GetSnsCanistersSummaryResponse, + }; pub fn list_sns_canisters( pocket_ic: &PocketIc, @@ -2018,6 +2021,30 @@ pub mod sns { }; Decode!(&result, ListSnsCanistersResponse).unwrap() } + + pub fn get_sns_canisters_summary( + pocket_ic: &PocketIc, + sns_root_canister_id: PrincipalId, + ) -> GetSnsCanistersSummaryResponse { + let result = pocket_ic + .update_call( + sns_root_canister_id.into(), + Principal::anonymous(), + "get_sns_canisters_summary", + Encode!(&GetSnsCanistersSummaryRequest { + update_canister_list: Some(false), + }) + .unwrap(), + ) + .unwrap(); + let result = match result { + WasmResult::Reply(result) => result, + WasmResult::Reject(s) => { + panic!("Call to get_sns_canisters_summary failed: {:#?}", s) + } + }; + Decode!(&result, GetSnsCanistersSummaryResponse).unwrap() + } } // Helper function that calls tick on env until either the index canister has synced all @@ -2120,7 +2147,10 @@ pub mod sns { use super::*; use assert_matches::assert_matches; use ic_nns_governance::pb::v1::create_service_nervous_system::SwapParameters; - use ic_sns_swap::{pb::v1::BuyerState, swap::principal_to_subaccount}; + use ic_sns_swap::{ + pb::v1::{BuyerState, GetOpenTicketRequest, GetOpenTicketResponse}, + swap::principal_to_subaccount, + }; use icp_ledger::DEFAULT_TRANSFER_FEE; pub fn get_init(pocket_ic: &PocketIc, canister_id: PrincipalId) -> GetInitResponse { @@ -2236,6 +2266,26 @@ pub mod sns { Ok(Decode!(&result, GetBuyerStateResponse).unwrap()) } + pub fn get_open_ticket( + pocket_ic: &PocketIc, + swap_canister_id: PrincipalId, + buyer: PrincipalId, + ) -> Result { + let result = pocket_ic + .query_call( + swap_canister_id.into(), + buyer.into(), + "get_open_ticket", + Encode!(&GetOpenTicketRequest {}).unwrap(), + ) + .map_err(|err| err.to_string())?; + let result = match result { + WasmResult::Reply(result) => result, + WasmResult::Reject(s) => panic!("Call to get_open_ticket failed: {:#?}", s), + }; + Ok(Decode!(&result, GetOpenTicketResponse).unwrap()) + } + pub fn error_refund_icp( pocket_ic: &PocketIc, swap_canister_id: PrincipalId, diff --git a/rs/nervous_system/integration_tests/tests/sns_lifecycle.rs b/rs/nervous_system/integration_tests/tests/sns_lifecycle.rs index fb2508abb52..461370a9296 100644 --- a/rs/nervous_system/integration_tests/tests/sns_lifecycle.rs +++ b/rs/nervous_system/integration_tests/tests/sns_lifecycle.rs @@ -4,7 +4,7 @@ use canister_test::Wasm; use ic_base_types::{CanisterId, PrincipalId}; use ic_ledger_core::Tokens; use ic_nervous_system_common::{ - assert_is_ok, ledger::compute_distribution_subaccount_bytes, E8, ONE_DAY_SECONDS, + assert_is_ok, i2d, ledger::compute_distribution_subaccount_bytes, E8, ONE_DAY_SECONDS, }; use ic_nervous_system_common_test_keys::TEST_NEURON_1_OWNER_PRINCIPAL; use ic_nervous_system_integration_tests::{ @@ -26,9 +26,10 @@ use ic_sns_init::distributions::MAX_DEVELOPER_DISTRIBUTION_COUNT; use ic_sns_swap::{ pb::v1::{ new_sale_ticket_response, set_dapp_controllers_call_result, set_mode_call_result, - settle_neurons_fund_participation_result, BuyerState, FinalizeSwapResponse, Lifecycle, - RefreshBuyerTokensResponse, SetDappControllersCallResult, SetDappControllersResponse, - SetModeCallResult, SettleNeuronsFundParticipationResult, SweepResult, + settle_neurons_fund_participation_result, BuyerState, FinalizeSwapResponse, + GetDerivedStateResponse, Lifecycle, RefreshBuyerTokensResponse, + SetDappControllersCallResult, SetDappControllersResponse, SetModeCallResult, + SettleNeuronsFundParticipationResult, SweepResult, }, swap::principal_to_subaccount, }; @@ -116,8 +117,8 @@ struct DirectParticipantConfig { /// 4.2. Unused portions of Neurons' Fund maturity reserved at SNS creation time are refunded. /// /// 5. Control over the dapp: -/// 5.1. `{ dapp_canister_status.controllers() == vec![developer] } FinalizeUnSuccessfully { dapp_canister_status.controllers() == vec![developer] }` -/// 5.2. `{ dapp_canister_status.controllers() == vec![developer] } FinalizeSuccessfully { dapp_canister_status.controllers() == vec![sns_governance] }` +/// 5.1. `{ dapp_canister_status.controllers() == vec![developer, nns_root] } FinalizeUnSuccessfully { dapp_canister_status.controllers() == vec![fallback_controllers] }` +/// 5.2. `{ dapp_canister_status.controllers() == vec![developer, nns_root] } FinalizeSuccessfully { dapp_canister_status.controllers() == vec![sns_root] }` /// /// 6. SNS neuron creation: /// 6.1. `{ true } FinalizeUnSuccessfully { No additional SNS neurons are created. }` @@ -168,6 +169,14 @@ fn test_sns_lifecycle( .unwrap() .e8s .unwrap(); + let expect_swap_overcommitted = { + let minimum_participants = swap_parameters.minimum_participants.unwrap(); + max_participant_icp_e8s as u128 * minimum_participants as u128 + > max_direct_participation_icp_e8s as u128 + }; + let expect_neurons_fund_participation = swap_parameters + .neurons_fund_participation + .unwrap_or_default(); let (developer_neuron_stake_sns_e8s, treasury_distribution_sns_e8s, swap_distribution_sns_e8s) = { let treasury_distribution_sns_e8s = initial_token_distribution .treasury_distribution @@ -320,22 +329,25 @@ fn test_sns_lifecycle( } // Check who has control over the dapp before the swap. - for dapp_canister_id in dapp_canister_ids.clone() { - let controllers: BTreeSet<_> = pocket_ic - .canister_status( - Principal::from(dapp_canister_id), - Some(Principal::from(ROOT_CANISTER_ID.get())), - ) - .unwrap() - .settings - .controllers - .into_iter() - .map(PrincipalId::from) - .collect(); - assert_eq!( - controllers, - original_controllers.clone().into_iter().collect() - ); + // This is very likely to succeed, because we just created the canisters a moment ago. + { + for dapp_canister_id in dapp_canister_ids.clone() { + let controllers: BTreeSet<_> = pocket_ic + .canister_status( + Principal::from(dapp_canister_id), + Some(Principal::from(ROOT_CANISTER_ID.get())), + ) + .unwrap() + .settings + .controllers + .into_iter() + .map(PrincipalId::from) + .collect(); + assert_eq!( + controllers, + original_controllers.clone().into_iter().collect() + ); + } } // 2. Create an SNS instance @@ -375,15 +387,13 @@ fn test_sns_lifecycle( let nervous_system_parameters = sns::governance::get_nervous_system_parameters(&pocket_ic, sns_governance_canister_id); - let sns_neurons_per_backet = { - let swap_init = sns::swap::get_init(&pocket_ic, swap_canister_id) - .init - .unwrap(); - swap_init - .neuron_basket_construction_parameters - .unwrap() - .count - }; + let swap_init = sns::swap::get_init(&pocket_ic, swap_canister_id) + .init + .unwrap(); + let sns_neurons_per_backet = swap_init + .neuron_basket_construction_parameters + .unwrap() + .count; // This set is used to determine SNS neurons created as a result of the swap (by excluding those // which are in this collection). @@ -452,6 +462,28 @@ fn test_sns_lifecycle( ); } + // Check that the dapp canisters are now controlled by SNS Root and NNS Root. + { + let expected_new_controllers = + BTreeSet::from([sns_root_canister_id, ROOT_CANISTER_ID.get()]); + for dapp_canister_id in dapp_canister_ids.clone() { + let sender = expected_new_controllers // the sender must be a controller + .first() + .cloned() + .map(Principal::from); + let controllers: BTreeSet<_> = pocket_ic + .canister_status(Principal::from(dapp_canister_id), sender) + .unwrap() + .settings + .controllers + .into_iter() + .map(PrincipalId::from) + .collect(); + + assert_eq!(controllers, expected_new_controllers); + } + } + // Currently, the neuron cannot start dissolving (an error is expected). { let start_dissolving_response = sns::governance::start_dissolving_neuron( @@ -511,8 +543,18 @@ fn test_sns_lifecycle( // Check that the derived state correctly reflects the pre-state of the swap. { let derived_state = sns::swap::get_derived_state(&pocket_ic, swap_canister_id); - assert_eq!(derived_state.direct_participation_icp_e8s.unwrap(), 0); - assert_eq!(derived_state.neurons_fund_participation_icp_e8s.unwrap(), 0); + assert_eq!( + derived_state, + GetDerivedStateResponse { + buyer_total_icp_e8s: Some(0), + direct_participant_count: Some(0), + cf_participant_count: Some(0), + cf_neuron_count: Some(0), + sns_tokens_per_icp: Some(0.0), + direct_participation_icp_e8s: Some(0), + neurons_fund_participation_icp_e8s: Some(0), + } + ); } // 3. Transfer ICP to our direct participants' SNSes subaccounts. @@ -654,10 +696,24 @@ fn test_sns_lifecycle( assert_eq!(icp.amount_e8s, expected_accepted_participation_amount_e8s); } + // Postcondition C: the ticket has been deleted. + { + let response = + sns::swap::get_open_ticket(&pocket_ic, swap_canister_id, direct_participant) + .expect("Swap.get_open_ticket response should be Ok."); + assert_eq!(response.ticket(), Ok(None)); + } + direct_sns_neuron_recipients.push(direct_participant); } - sns::swap::await_swap_lifecycle(&pocket_ic, swap_canister_id, Lifecycle::Committed) - .unwrap(); + + // In this runbook, all participants participate s.t. `max_participant_icp_e8s` is reached. + let expected_lifecycle = if expect_swap_overcommitted { + Lifecycle::Aborted + } else { + Lifecycle::Committed + }; + sns::swap::await_swap_lifecycle(&pocket_ic, swap_canister_id, expected_lifecycle).unwrap(); direct_sns_neuron_recipients }; @@ -667,11 +723,12 @@ fn test_sns_lifecycle( // It may take some time for the process to complete, so we should await (implemented via a busy // loop) rather than try just once. let swap_finalization_status = { - let expected_swap_finalization_status = if ensure_swap_timeout_is_reached { - SwapFinalizationStatus::Aborted - } else { - SwapFinalizationStatus::Committed - }; + let expected_swap_finalization_status = + if ensure_swap_timeout_is_reached || expect_swap_overcommitted { + SwapFinalizationStatus::Aborted + } else { + SwapFinalizationStatus::Committed + }; if let Err(err) = sns::swap::await_swap_finalization_status( &pocket_ic, swap_canister_id, @@ -717,7 +774,18 @@ fn test_sns_lifecycle( .expect("Error while calling Swap.error_refund_icp"); use ic_sns_swap::pb::v1::error_refund_icp_response; - let expected_refund_e8s = if swap_finalization_status == SwapFinalizationStatus::Aborted { + + // Notes to help understand this spec: + // 1. Currently, Swap.error_refund_icp returns an error from ICP Ledger if the amount + // to reimburse is zero (or less than the transfer fee). + // 2. Currently, when `ensure_swap_timeout_is_reached` is true, none of the direct + // participants call Swap.refresh_buyer_tokens before the timeout, so their ICP is still + // to be refunded by calling Swap.error_refund_icp (case A). + // 3. Conversely, when `ensure_swap_timeout_is_reached` is false and + // `expect_swap_overcommitted` is true, Swap.sweep_icp takes care of all the refunds, + // so there's no more refunds that can happen in Swap.error_refund_icp, which thus + // returns an error (case B). + let expected_refund_e8s = if ensure_swap_timeout_is_reached { // Case A: Expecting to get refunded with Transferred - (ICP Ledger transfer fee). assert_matches!( error_refund_icp_result, @@ -740,7 +808,11 @@ fn test_sns_lifecycle( "the debit account doesn't have enough funds to complete the transaction" )); - 0 + if expect_swap_overcommitted { + attempted_participation_amount_e8s - DEFAULT_TRANSFER_FEE.get_e8s() + } else { + 0 + } } else { // Case C: Expecting to get refunded with Transferred - Accepted - (ICP Ledger transfer fee). assert_matches!( @@ -768,10 +840,7 @@ fn test_sns_lifecycle( let expected_neuron_count = if swap_finalization_status == SwapFinalizationStatus::Aborted { 0 } else { - let swap_participating_nns_neuron_count = if swap_parameters - .neurons_fund_participation - .unwrap_or_default() - { + let swap_participating_nns_neuron_count = if expect_neurons_fund_participation { direct_participants.len() as u128 + neurons_fund_nns_neurons.len() as u128 } else { direct_participants.len() as u128 @@ -782,7 +851,7 @@ fn test_sns_lifecycle( let expected_sweep_icp_result = Some(SweepResult { success: 0, failure: 0, - skipped: if swap_finalization_status == SwapFinalizationStatus::Aborted { + skipped: if ensure_swap_timeout_is_reached { 0 } else { direct_participants.len() as u32 @@ -844,9 +913,7 @@ fn test_sns_lifecycle( let expected_settle_neurons_fund_participation_result = { let (neurons_fund_participation_icp_e8s, neurons_fund_neurons_count) = if swap_finalization_status == SwapFinalizationStatus::Committed - && swap_parameters - .neurons_fund_participation - .unwrap_or_default() + && expect_neurons_fund_participation { ( Some(150_000 * E8), @@ -891,15 +958,86 @@ fn test_sns_lifecycle( } // Inspect the final derived state - let derived_state = sns::swap::get_derived_state(&pocket_ic, swap_canister_id); - if swap_finalization_status == SwapFinalizationStatus::Aborted { - assert_eq!(derived_state.direct_participation_icp_e8s.unwrap(), 0); - } else { + { + // Declare the expectations for all relevant fields. + let dpc = || direct_participants.len() as u64; + // For cf_participant_count. + let nfpc = || nns_controller_to_neurons_fund_neurons.keys().len() as u64; + // For cf_neuron_count. + let nfnc = || nns_controller_to_neurons_fund_neurons.values().len() as u64; + let ( + direct_participant_count, + direct_participation_icp_e8s, + cf_participant_count, + cf_neuron_count, + neurons_fund_participation_icp_e8s, + buyer_total_icp_e8s, + ) = match ( + ensure_swap_timeout_is_reached, + expect_swap_overcommitted, + expect_neurons_fund_participation, + ) { + (true, true, _) => { + // Only !(ensure_swap_timeout_is_reached ^ expect_swap_overcommitted) scenarios + // are currently supported. + unimplemented!(); + } + (true, false, _) => (Some(0), Some(0), Some(0), Some(0), Some(0), Some(0)), + (false, true, true) => { + // The Neurons' Fund is orthogonal to the overpayment scenario. + unimplemented!(); + } + (false, true, false) => ( + Some(dpc()), + Some(650_000 * E8), + Some(0), + Some(0), + Some(0), + Some(650_000 * E8), + ), + (false, false, true) => ( + Some(dpc()), + Some(650_000 * E8), + Some(nfpc()), + Some(nfnc()), + Some(150_000 * E8), + Some(800_000 * E8), + ), + (false, false, false) => ( + Some(dpc()), + Some(650_000 * E8), + Some(0), + Some(0), + Some(0), + Some(650_000 * E8), + ), + }; + let sns_tokens_per_icp = Some( + buyer_total_icp_e8s + .map(|buyer_total_icp_e8s| { + let sns_token_e8s = swap_init.sns_token_e8s.unwrap(); + i2d(sns_token_e8s) + .checked_div(i2d(buyer_total_icp_e8s)) + .and_then(|d| d.to_f32()) + .unwrap_or(0.0) + }) + .unwrap_or(0.0) as f64, + ); + + let observed_derived_state = sns::swap::get_derived_state(&pocket_ic, swap_canister_id); assert_eq!( - derived_state.direct_participation_icp_e8s.unwrap(), - 650_000 * E8 + observed_derived_state, + GetDerivedStateResponse { + direct_participant_count, + direct_participation_icp_e8s, + cf_participant_count, + cf_neuron_count, + neurons_fund_participation_icp_e8s, + buyer_total_icp_e8s, + sns_tokens_per_icp, + } ); - } + }; // Assert that the mode of SNS Governance is correct if swap_finalization_status == SwapFinalizationStatus::Aborted { @@ -918,6 +1056,23 @@ fn test_sns_lifecycle( ); } + // Validate `get_sns_canisters_summary`. + { + let response = sns::root::get_sns_canisters_summary(&pocket_ic, sns_root_canister_id); + let observed_dapp_canister_ids = response + .dapps + .into_iter() + .map(|canister_summary| { + CanisterId::unchecked_from_principal(canister_summary.canister_id.unwrap()) + }) + .collect::>(); + if swap_finalization_status == SwapFinalizationStatus::Aborted { + assert_eq!(observed_dapp_canister_ids, vec![]); + } else { + assert_eq!(observed_dapp_canister_ids, dapp_canister_ids); + } + } + // Ensure that the proposal submission is possible if and only if the SNS governance has // launched, and that `PreInitializationSwap` mode limitations are still in place if and only // if the swap aborted. @@ -1072,10 +1227,7 @@ fn test_sns_lifecycle( let neurons_fund_neuron_controllers_to_neuron_portions: BTreeMap< PrincipalId, NeuronsFundNeuronPortion, - > = if swap_parameters - .neurons_fund_participation - .unwrap_or_default() - { + > = if expect_neurons_fund_participation { let Some(get_neurons_fund_audit_info_response::Result::Ok( get_neurons_fund_audit_info_response::Ok { neurons_fund_audit_info: Some(neurons_fund_audit_info), @@ -1151,10 +1303,7 @@ fn test_sns_lifecycle( // ``` expected_neuron_controller_principal_ids .extend(neurons_fund_neuron_controller_principal_ids.iter()); - if swap_parameters - .neurons_fund_participation - .unwrap_or_default() - { + if expect_neurons_fund_participation { // NNS Governance is the expected controller of SNS neurons created for // the Neurons' Fund participants. expected_neuron_controller_principal_ids.insert(GOVERNANCE_CANISTER_ID.get()); @@ -1484,36 +1633,8 @@ fn test_sns_lifecycle( } } - if swap_parameters - .neurons_fund_participation - .unwrap_or_default() - { - if swap_finalization_status == SwapFinalizationStatus::Aborted { - assert_eq!( - derived_state.neurons_fund_participation_icp_e8s.unwrap(), - 0, - "Neurons' Fund participation should not be provided to an aborted SNS swap.", - ); - } else { - assert_eq!( - derived_state.neurons_fund_participation_icp_e8s.unwrap(), - 150_000 * E8, - "Neurons' Fund participation is expected to be at 10% of its total maturity.", - ); - } - } else { - assert_eq!( - derived_state.neurons_fund_participation_icp_e8s.unwrap(), - 0, - "Neurons' Fund participation has not been requested, yet there is some.", - ); - } - // Check that the maturity of the Neurons' Fund neurons adds up. - if swap_parameters - .neurons_fund_participation - .unwrap_or_default() - { + if expect_neurons_fund_participation { let Some(get_neurons_fund_audit_info_response::Result::Ok( get_neurons_fund_audit_info_response::Ok { neurons_fund_audit_info: Some(neurons_fund_audit_info), @@ -1628,28 +1749,31 @@ fn test_sns_lifecycle( } // Check who has control over the dapp after the swap. - let expected_new_controllers = if swap_finalization_status == SwapFinalizationStatus::Aborted { - // The SNS swap has failed ==> control should be returned to the fallback controllers. - fallback_controllers.into_iter().collect::>() - } else { - // The SNS swap has succeeded ==> root should have sole control. - BTreeSet::from([sns_root_canister_id]) - }; - for dapp_canister_id in dapp_canister_ids { - let sender = expected_new_controllers // the sender must be a controller - .first() - .cloned() - .map(Principal::from); - let controllers: BTreeSet<_> = pocket_ic - .canister_status(Principal::from(dapp_canister_id), sender) - .unwrap() - .settings - .controllers - .into_iter() - .map(PrincipalId::from) - .collect(); + { + let expected_new_controllers = + if swap_finalization_status == SwapFinalizationStatus::Aborted { + // The SNS swap has failed ==> control should be returned to the fallback controllers. + fallback_controllers.into_iter().collect::>() + } else { + // The SNS swap has succeeded ==> root should have sole control. + BTreeSet::from([sns_root_canister_id]) + }; + for dapp_canister_id in dapp_canister_ids { + let sender = expected_new_controllers // the sender must be a controller + .first() + .cloned() + .map(Principal::from); + let controllers: BTreeSet<_> = pocket_ic + .canister_status(Principal::from(dapp_canister_id), sender) + .unwrap() + .settings + .controllers + .into_iter() + .map(PrincipalId::from) + .collect(); - assert_eq!(controllers, expected_new_controllers); + assert_eq!(controllers, expected_new_controllers); + } } } @@ -1691,7 +1815,20 @@ fn test_sns_lifecycle_happy_scenario_without_neurons_fund_participation() { } #[test] -fn test_sns_lifecycle_happy_scenario_wih_dapp_canisters() { +fn test_sns_lifecycle_overpayment_scenario() { + test_sns_lifecycle( + false, + CreateServiceNervousSystemBuilder::default() + .neurons_fund_participation(false) + .with_minimum_participants(2) + .with_dapp_canisters(vec![CanisterId::from_u64(100)]) + .build(), + btreemap! { PrincipalId::new_user_test_id(1) => DirectParticipantConfig { use_ticketing_system: true } }, + ); +} + +#[test] +fn test_sns_lifecycle_happy_scenario_with_dapp_canisters() { test_sns_lifecycle( false, CreateServiceNervousSystemBuilder::default() diff --git a/rs/tests/nns/sns/BUILD.bazel b/rs/tests/nns/sns/BUILD.bazel index 25d78e16b12..5e2e26a1ed2 100644 --- a/rs/tests/nns/sns/BUILD.bazel +++ b/rs/tests/nns/sns/BUILD.bazel @@ -131,26 +131,6 @@ system_test( deps = DEPENDENCIES + ["//rs/tests"], ) -system_test( - name = "overpayment_fail_test", - flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = GUESTOS_RUNTIME_DEPS + NNS_CANISTER_RUNTIME_DEPS + - SNS_CANISTER_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], -) - -system_test( - name = "payment_flow_with_finalization_test", - flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = GUESTOS_RUNTIME_DEPS + NNS_CANISTER_RUNTIME_DEPS + - SNS_CANISTER_RUNTIME_DEPS + GRAFANA_RUNTIME_DEPS, - deps = DEPENDENCIES + ["//rs/tests"], -) - system_test( name = "payment_flow_load_test", proc_macro_deps = MACRO_DEPENDENCIES, diff --git a/rs/tests/nns/sns/Cargo.toml b/rs/tests/nns/sns/Cargo.toml index bcf2f1226cb..83c88fb0d25 100644 --- a/rs/tests/nns/sns/Cargo.toml +++ b/rs/tests/nns/sns/Cargo.toml @@ -52,22 +52,10 @@ path = "launchpad_direct_auth_load_test.rs" name = "ic-systest-sns-launchpad-aggregator-load-test" path = "launchpad_aggregator_load_test.rs" -[[bin]] -name = "ic-systest-sns-payment-flow-with-finalization-test" -path = "payment_flow_with_finalization_test.rs" - -[[bin]] -name = "ic-systest-sns-one-proposal-payment-flow-with-finalization-test" -path = "payment_flow_with_finalization_test.rs" - [[bin]] name = "ic-systest-sns-one-proposal-neurons-fund-swap-participation-test" path = "neurons_fund_swap_participation_test.rs" -[[bin]] -name = "ic-systest-sns-one-proposal-overpayment-fail-test" -path = "overpayment_fail_test.rs" - [[bin]] name = "ic-systest-sns-payment-flow-load-test" path = "payment_flow_load_test.rs" diff --git a/rs/tests/nns/sns/overpayment_fail_test.rs b/rs/tests/nns/sns/overpayment_fail_test.rs deleted file mode 100644 index fafb0d87123..00000000000 --- a/rs/tests/nns/sns/overpayment_fail_test.rs +++ /dev/null @@ -1,166 +0,0 @@ -use anyhow::Result; -use ic_nervous_system_common::i2d; -use ic_nns_governance::pb::v1::create_service_nervous_system::SwapParameters; -use ic_nns_governance::pb::v1::CreateServiceNervousSystem; -use ic_sns_swap::pb::v1::GetDerivedStateResponse; -use ic_tests::driver::group::SystemTestGroup; -use ic_tests::driver::test_env::TestEnv; -use ic_tests::driver::test_env_api::NnsCanisterWasmStrategy; -use ic_tests::nns_tests::neurons_fund; -use ic_tests::nns_tests::neurons_fund::NnsNfNeuron; -use ic_tests::nns_tests::{ - sns_deployment, sns_deployment::generate_ticket_participants_workload, swap_finalization, - swap_finalization::finalize_aborted_swap_and_check_success, -}; -use ic_tests::sns_client::test_create_service_nervous_system_proposal; -use ic_tests::systest; -use ic_tests::util::block_on; -use rust_decimal::prelude::ToPrimitive; -use std::time::Duration; - -fn create_service_nervous_system_proposal() -> CreateServiceNervousSystem { - const MIN_PARTICIPANTS: u64 = 4; - let csns = test_create_service_nervous_system_proposal(MIN_PARTICIPANTS); - let swap_parameters = csns.swap_parameters.clone().unwrap(); - CreateServiceNervousSystem { - swap_parameters: Some(SwapParameters { - maximum_participant_icp: swap_parameters.maximum_direct_participation_icp, - ..swap_parameters - }), - ..csns - } -} - -fn nns_nf_neurons() -> Vec { - let max_nf_contribution = create_service_nervous_system_proposal() - .swap_parameters - .unwrap() - .maximum_direct_participation_icp - .unwrap() - .e8s - .unwrap() - * 2; - - neurons_fund::initial_nns_neurons(max_nf_contribution * 10, 1) -} - -fn sns_setup_with_one_proposal(env: TestEnv) { - sns_deployment::setup( - &env, - vec![], - nns_nf_neurons(), - create_service_nervous_system_proposal(), - NnsCanisterWasmStrategy::TakeBuiltFromSources, - true, - ); -} - -fn wait_for_swap_to_start(env: TestEnv) { - block_on(swap_finalization::wait_for_swap_to_start(&env)); -} - -/// Creates ticket participants which will contribute in such a way that they'll hit max_icp_e8s with min_participants. -/// So if min_participants is 3 and max_participant_icp_e8s is 12 icp, we'll create 3 participants who contribute 4 icp each. -fn generate_ticket_participants_workload_necessary_to_abort_the_swap(env: TestEnv) { - let swap_params = create_service_nervous_system_proposal() - .swap_parameters - .unwrap(); - assert_ne!( - swap_params.minimum_participants.unwrap(), - 0, - "min_participants must be greater than zero!" - ); - // We'll have the test use params.min_participants as the number of - // participants, to allow the swap to have enough participants to close. - let num_participants = 1; - assert!(num_participants < swap_params.minimum_participants.unwrap()); - - // Calculate a value for `contribution_per_user` that will cause the icp - // raised by the swap to exactly equal `params.max_icp_e8s - cf_contribution`. - // Since we won't have reached swap_params.minimum_participants, this will - // cause the swap to fail - let contribution_per_user = swap_params - .maximum_direct_participation_icp - .unwrap() - .e8s - .unwrap(); - - // The number of participants is the rps * the duration in seconds. - // So if we set rps to `1`, and the duration to `num_participants`, we'll - // have `num_participants` participants. - generate_ticket_participants_workload( - &env, - 1, - Duration::from_secs(num_participants), - contribution_per_user, - ); -} - -fn finalize_swap(env: TestEnv) { - let create_service_nervous_system_proposal = create_service_nervous_system_proposal(); - let swap_params = create_service_nervous_system_proposal - .swap_parameters - .as_ref() - .unwrap() - .clone(); - - let sns_tokens_per_icp = i2d(create_service_nervous_system_proposal - .sns_token_e8s() - .unwrap()) - .checked_div(i2d(swap_params - .maximum_direct_participation_icp - .unwrap() - .e8s - .unwrap())) - .and_then(|d| d.to_f32()) - .unwrap() as f64; - - let expected_derived_swap_state = GetDerivedStateResponse { - direct_participant_count: Some(1), - cf_participant_count: Some(0), - cf_neuron_count: Some(0), - buyer_total_icp_e8s: swap_params.maximum_direct_participation_icp.unwrap().e8s, - sns_tokens_per_icp: Some(sns_tokens_per_icp), - direct_participation_icp_e8s: Some( - swap_params.maximum_direct_participation_icp.unwrap().e8s(), - ), - neurons_fund_participation_icp_e8s: Some(0), - }; - - block_on(finalize_aborted_swap_and_check_success( - env, - expected_derived_swap_state, - create_service_nervous_system_proposal, - )); -} - -/// This test is similar to //rs/tests/nns/sns:payment_flow_with_finalization_test, except it causes the swap to be aborted -/// and also finalizes the swap. -/// A load test is currently not possible because finalization is too slow. -/// -/// Runbook: -/// 1. Install NNS (with N users, each with X ICP) and SNS -/// * N = NUM_SNS_SALE_PARTICIPANTS -/// * SNS_SALE_PARAM_MIN_PARTICIPANT_ICP_E8S <= X <= SNS_SALE_PARAM_MAX_PARTICIPANT_ICP_E8S -/// 2. Initiate Token Swap -/// 3. Transfer X ICP to the test user's swap sub-account of the SNS swap canister -/// 4. Create a token swap ticket (of X ICP) for the test user -/// 5. Initiate the participation of the test user -/// 6. Assert that the user is actually participating in the swap of X ICP worth of SNS tokens -/// 7. Finalize the swap -/// -/// Some `get_open_ticket failed: SaleClosed` messages are expected. -/// These occur when the test is trying to assert that the user's ticket has been deleted, but the -/// swap has already closed. -fn main() -> Result<()> { - SystemTestGroup::new() - .with_overall_timeout(Duration::from_secs(15 * 60)) // 15 min - .with_setup(sns_setup_with_one_proposal) - .add_test(systest!(wait_for_swap_to_start)) - .add_test(systest!( - generate_ticket_participants_workload_necessary_to_abort_the_swap - )) - .add_test(systest!(finalize_swap)) - .execute_from_args()?; - Ok(()) -} diff --git a/rs/tests/nns/sns/payment_flow_with_finalization_test.rs b/rs/tests/nns/sns/payment_flow_with_finalization_test.rs deleted file mode 100644 index 8f5979344cb..00000000000 --- a/rs/tests/nns/sns/payment_flow_with_finalization_test.rs +++ /dev/null @@ -1,130 +0,0 @@ -use anyhow::Result; -use ic_nns_governance::pb::v1::CreateServiceNervousSystem; -use ic_tests::driver::group::SystemTestGroup; -use ic_tests::driver::test_env::TestEnv; -use ic_tests::driver::test_env_api::NnsCanisterWasmStrategy; -use ic_tests::nns_tests::neurons_fund; -use ic_tests::nns_tests::neurons_fund::NnsNfNeuron; -use ic_tests::nns_tests::{ - sns_deployment, sns_deployment::generate_ticket_participants_workload, swap_finalization, - swap_finalization::finalize_committed_swap, -}; -use ic_tests::sns_client::test_create_service_nervous_system_proposal; -use ic_tests::systest; -use ic_tests::util::block_on; -use std::time::Duration; - -fn create_service_nervous_system_proposal() -> CreateServiceNervousSystem { - // The higher the value for MIN_PARTICIPANTS, the longer the test will take. - // But lower values are less realistic. In the long term, we should have a - // load test that uses a high value for MIN_PARTICIPANTS, but 4 is high enough - // to still discover many potential problems. - const MIN_PARTICIPANTS: u64 = 4; - test_create_service_nervous_system_proposal(MIN_PARTICIPANTS) -} - -fn nns_nf_neurons() -> Vec { - let max_nf_contribution = create_service_nervous_system_proposal() - .swap_parameters - .unwrap() - .maximum_direct_participation_icp - .unwrap() - .e8s - .unwrap() - * 2; - - neurons_fund::initial_nns_neurons(max_nf_contribution * 10, 1) -} - -fn sns_setup_with_one_proposal(env: TestEnv) { - sns_deployment::setup( - &env, - vec![], - nns_nf_neurons(), - create_service_nervous_system_proposal(), - NnsCanisterWasmStrategy::TakeBuiltFromSources, - true, - ); -} - -fn wait_for_swap_to_start(env: TestEnv) { - block_on(swap_finalization::wait_for_swap_to_start(&env)); -} - -/// Creates ticket participants which will contribute in such a way that they'll hit max_icp_e8s with min_participants. -/// So if min_participants is 3 and max_participant_icp_e8s is 12 icp, we'll create 3 participants who contribute 4 icp each. -fn generate_ticket_participants_workload_necessary_to_close_the_swap(env: TestEnv) { - let swap_params = create_service_nervous_system_proposal() - .swap_parameters - .unwrap(); - assert_ne!( - swap_params.minimum_participants.unwrap(), - 0, - "min_participants must be greater than zero!" - ); - // We'll have the test use params.min_participants as the number of - // participants, to allow the swap to have enough participants to close. - let num_participants = swap_params.minimum_participants.unwrap(); - - // Calculate a value for `contribution_per_user` that will cause the icp - // raised by the swap to exactly equal `params.max_icp_e8s`. - let contribution_per_user = ic_tests::util::divide_perfectly( - "maximum_direct_participation_icp", - swap_params - .maximum_direct_participation_icp - .unwrap() - .e8s - .unwrap(), - num_participants, - ) - .unwrap(); - - // The number of participants is the rps * the duration in seconds. - // So if we set rps to `1`, and the duration to `num_participants`, we'll - // have `num_participants` participants. - generate_ticket_participants_workload( - &env, - 1, - Duration::from_secs(num_participants), - contribution_per_user, - ); -} - -fn finalize_swap(env: TestEnv) { - let create_service_nervous_system_proposal = create_service_nervous_system_proposal(); - block_on(finalize_committed_swap( - &env, - create_service_nervous_system_proposal, - )); -} - -/// Creates an SNS and participates it enough to close the swap, then checks that the swap -/// finalizes as expected. -/// A load test that includes finalization is currently not possible because finalization is too slow. -/// -/// Runbook: -/// 1. Install NNS (with N users, each with X ICP) and SNS -/// * N = NUM_SNS_SALE_PARTICIPANTS -/// * SNS_SALE_PARAM_MIN_PARTICIPANT_ICP_E8S <= X <= SNS_SALE_PARAM_MAX_PARTICIPANT_ICP_E8S -/// 2. Initiate Token Swap -/// 3. Transfer X ICP to the test user's swap sub-account of the SNS swap canister -/// 4. Create a token swap ticket (of X ICP) for the test user -/// 5. Initiate the participation of the test user -/// 6. Assert that the user is actually participating in the swap of X ICP worth of SNS tokens -/// 7. Finalize the swap -/// -/// Some `get_open_ticket failed: SaleClosed` messages are expected. -/// These occur when the test is trying to assert that the user's ticket has been deleted, but the -/// swap has already closed. -fn main() -> Result<()> { - SystemTestGroup::new() - .with_overall_timeout(Duration::from_secs(15 * 60)) // 15 min - .with_setup(sns_setup_with_one_proposal) - .add_test(systest!(wait_for_swap_to_start)) - .add_test(systest!( - generate_ticket_participants_workload_necessary_to_close_the_swap - )) - .add_test(systest!(finalize_swap)) - .execute_from_args()?; - Ok(()) -}