From f84b9090add36ff89f9c5bab3780b89668022112 Mon Sep 17 00:00:00 2001 From: Andre Popovitch Date: Mon, 30 Sep 2024 15:20:44 -0500 Subject: [PATCH 1/2] Implement additional helpers in ic-nervous-system-agent --- Cargo.lock | 3 + rs/nervous_system/agent/BUILD.bazel | 3 + rs/nervous_system/agent/Cargo.toml | 3 + rs/nervous_system/agent/src/agent_impl.rs | 2 + rs/nervous_system/agent/src/lib.rs | 11 ++- rs/nervous_system/agent/src/nns/governance.rs | 16 ++++ rs/nervous_system/agent/src/nns/mod.rs | 1 + rs/nervous_system/agent/src/pocketic_impl.rs | 2 + rs/nervous_system/agent/src/sns/governance.rs | 7 ++ rs/nervous_system/agent/src/sns/index.rs | 7 ++ rs/nervous_system/agent/src/sns/ledger.rs | 7 ++ rs/nervous_system/agent/src/sns/root.rs | 5 ++ rs/nervous_system/agent/src/sns/swap.rs | 75 +++++++++++++++++++ rs/nns/governance/api/src/lib.rs | 1 + rs/nns/governance/api/src/request_impls.rs | 7 ++ rs/sns/swap/src/lib.rs | 1 + rs/sns/swap/src/request_impls.rs | 19 +++++ 17 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 rs/nervous_system/agent/src/nns/governance.rs create mode 100644 rs/nns/governance/api/src/request_impls.rs create mode 100644 rs/sns/swap/src/request_impls.rs diff --git a/Cargo.lock b/Cargo.lock index 067e8b126a1..c3e3aa5b0bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9526,9 +9526,12 @@ dependencies = [ "ic-agent", "ic-base-types", "ic-nervous-system-clients", + "ic-nns-common", "ic-nns-constants", + "ic-nns-governance-api", "ic-sns-governance", "ic-sns-root", + "ic-sns-swap", "ic-sns-wasm", "pocket-ic", "serde", diff --git a/rs/nervous_system/agent/BUILD.bazel b/rs/nervous_system/agent/BUILD.bazel index 88fc914ac53..29fcc996251 100644 --- a/rs/nervous_system/agent/BUILD.bazel +++ b/rs/nervous_system/agent/BUILD.bazel @@ -6,10 +6,13 @@ DEPENDENCIES = [ # Keep sorted. "//packages/pocket-ic", "//rs/nervous_system/clients", + "//rs/nns/common", "//rs/nns/constants", + "//rs/nns/governance/api", "//rs/nns/sns-wasm", "//rs/sns/governance", "//rs/sns/root", + "//rs/sns/swap", "//rs/types/base_types", "@crate_index//:anyhow", "@crate_index//:candid", diff --git a/rs/nervous_system/agent/Cargo.toml b/rs/nervous_system/agent/Cargo.toml index 6fd6bf16418..b8e2557c7de 100644 --- a/rs/nervous_system/agent/Cargo.toml +++ b/rs/nervous_system/agent/Cargo.toml @@ -13,11 +13,14 @@ candid = { workspace = true } ic-agent = { workspace = true } ic-base-types = { path = "../../types/base_types" } ic-nervous-system-clients = { path = "../clients" } +ic-nns-governance-api = { path = "../../nns/governance/api" } +ic-nns-common = { path = "../../nns/common" } ic-nns-constants = { path = "../../nns/constants" } ic-sns-wasm = { path = "../../nns/sns-wasm" } ic-sns-governance = { path = "../../sns/governance" } pocket-ic = { path = "../../../packages/pocket-ic" } ic-sns-root = { path = "../../sns/root" } +ic-sns-swap = { path = "../../sns/swap" } serde = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } diff --git a/rs/nervous_system/agent/src/agent_impl.rs b/rs/nervous_system/agent/src/agent_impl.rs index 2f050e5444b..6c47afc2319 100644 --- a/rs/nervous_system/agent/src/agent_impl.rs +++ b/rs/nervous_system/agent/src/agent_impl.rs @@ -15,6 +15,8 @@ pub enum AgentCallError { CandidDecode(candid::Error), } +impl crate::sealed::Sealed for Agent {} + impl CallCanisters for Agent { type Error = AgentCallError; async fn call( diff --git a/rs/nervous_system/agent/src/lib.rs b/rs/nervous_system/agent/src/lib.rs index 29bb33a4082..d90b0b4d69f 100644 --- a/rs/nervous_system/agent/src/lib.rs +++ b/rs/nervous_system/agent/src/lib.rs @@ -7,8 +7,15 @@ use candid::Principal; use ic_nervous_system_clients::Request; use std::fmt::Display; -pub trait CallCanisters { - type Error: Display + Send; +// This is used to "seal" the CallCanisters trait so that it cannot be implemented outside of this crate. +// This is useful because it means we can modify the trait in the future without worrying about +// breaking backwards compatibility with implementations outside of this crate. +mod sealed { + pub trait Sealed {} +} + +pub trait CallCanisters: sealed::Sealed { + type Error: Display + Send + std::error::Error + 'static; fn call( &self, canister_id: impl Into + Send, diff --git a/rs/nervous_system/agent/src/nns/governance.rs b/rs/nervous_system/agent/src/nns/governance.rs new file mode 100644 index 00000000000..7f9786153a0 --- /dev/null +++ b/rs/nervous_system/agent/src/nns/governance.rs @@ -0,0 +1,16 @@ +use crate::CallCanisters; +use ic_nns_common::pb::v1::ProposalId; +use ic_nns_constants::GOVERNANCE_CANISTER_ID; +use ic_nns_governance_api::pb::v1::{ + GetNeuronsFundAuditInfoRequest, GetNeuronsFundAuditInfoResponse, +}; + +pub async fn get_neurons_fund_audit_info( + agent: &C, + nns_proposal_id: ProposalId, +) -> Result { + let request = GetNeuronsFundAuditInfoRequest { + nns_proposal_id: Some(nns_proposal_id), + }; + agent.call(GOVERNANCE_CANISTER_ID, request).await +} diff --git a/rs/nervous_system/agent/src/nns/mod.rs b/rs/nervous_system/agent/src/nns/mod.rs index 341405efcb0..280c372d50c 100644 --- a/rs/nervous_system/agent/src/nns/mod.rs +++ b/rs/nervous_system/agent/src/nns/mod.rs @@ -1 +1,2 @@ +pub mod governance; pub mod sns_wasm; diff --git a/rs/nervous_system/agent/src/pocketic_impl.rs b/rs/nervous_system/agent/src/pocketic_impl.rs index cfb9907571e..bbdcb32bb2e 100644 --- a/rs/nervous_system/agent/src/pocketic_impl.rs +++ b/rs/nervous_system/agent/src/pocketic_impl.rs @@ -17,6 +17,8 @@ pub enum PocketIcCallError { CandidDecode(candid::Error), } +impl crate::sealed::Sealed for PocketIc {} + impl CallCanisters for PocketIc { type Error = PocketIcCallError; async fn call( diff --git a/rs/nervous_system/agent/src/sns/governance.rs b/rs/nervous_system/agent/src/sns/governance.rs index 9d5e6e6ced5..8783ac0eed5 100644 --- a/rs/nervous_system/agent/src/sns/governance.rs +++ b/rs/nervous_system/agent/src/sns/governance.rs @@ -28,3 +28,10 @@ impl GovernanceCanister { .await } } + +impl GovernanceCanister { + pub fn new(canister_id: impl Into) -> Self { + let canister_id = canister_id.into(); + Self { canister_id } + } +} diff --git a/rs/nervous_system/agent/src/sns/index.rs b/rs/nervous_system/agent/src/sns/index.rs index 18324670829..ec231e7ef53 100644 --- a/rs/nervous_system/agent/src/sns/index.rs +++ b/rs/nervous_system/agent/src/sns/index.rs @@ -5,3 +5,10 @@ use serde::{Deserialize, Serialize}; pub struct IndexCanister { pub canister_id: PrincipalId, } + +impl IndexCanister { + pub fn new(canister_id: impl Into) -> Self { + let canister_id = canister_id.into(); + Self { canister_id } + } +} diff --git a/rs/nervous_system/agent/src/sns/ledger.rs b/rs/nervous_system/agent/src/sns/ledger.rs index 41a5b0a6f31..d5ff1ceb2b6 100644 --- a/rs/nervous_system/agent/src/sns/ledger.rs +++ b/rs/nervous_system/agent/src/sns/ledger.rs @@ -5,3 +5,10 @@ use serde::{Deserialize, Serialize}; pub struct LedgerCanister { pub canister_id: PrincipalId, } + +impl LedgerCanister { + pub fn new(canister_id: impl Into) -> Self { + let canister_id = canister_id.into(); + Self { canister_id } + } +} diff --git a/rs/nervous_system/agent/src/sns/root.rs b/rs/nervous_system/agent/src/sns/root.rs index a1aa270b9bf..52596deaac0 100644 --- a/rs/nervous_system/agent/src/sns/root.rs +++ b/rs/nervous_system/agent/src/sns/root.rs @@ -10,6 +10,11 @@ pub struct RootCanister { } impl RootCanister { + pub fn new(canister_id: impl Into) -> Self { + let canister_id = canister_id.into(); + Self { canister_id } + } + pub async fn sns_canisters_summary( &self, agent: &C, diff --git a/rs/nervous_system/agent/src/sns/swap.rs b/rs/nervous_system/agent/src/sns/swap.rs index d9080e28e19..48bcf820bc9 100644 --- a/rs/nervous_system/agent/src/sns/swap.rs +++ b/rs/nervous_system/agent/src/sns/swap.rs @@ -1,7 +1,82 @@ use ic_base_types::PrincipalId; +use ic_sns_swap::pb::v1::{ + GetDerivedStateRequest, GetDerivedStateResponse, GetInitRequest, GetInitResponse, + ListSnsNeuronRecipesRequest, ListSnsNeuronRecipesResponse, SnsNeuronRecipe, +}; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::CallCanisters; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct SwapCanister { pub canister_id: PrincipalId, } + +#[derive(Debug, Error)] +pub enum ListAllSnsNeuronRecipesError { + #[error(transparent)] + CanisterCallError(#[from] CallCanistersError), + #[error("There seem to be too many neuron recipes ({0}).")] + TooManyRecipes(u64), +} + +impl SwapCanister { + pub fn new(canister_id: impl Into) -> Self { + let canister_id = canister_id.into(); + Self { canister_id } + } + + pub async fn get_derived_state( + &self, + agent: &C, + ) -> Result { + agent + .call(self.canister_id, GetDerivedStateRequest {}) + .await + } + + pub async fn get_init(&self, agent: &C) -> Result { + agent.call(self.canister_id, GetInitRequest {}).await + } + + pub async fn list_sns_neuron_recipes( + &self, + agent: &C, + limit: u32, + offset: u64, + ) -> Result { + agent + .call( + self.canister_id, + ListSnsNeuronRecipesRequest { + limit: Some(limit), + offset: Some(offset), + }, + ) + .await + } + + pub async fn list_all_sns_neuron_recipes( + &self, + agent: &C, + ) -> Result, ListAllSnsNeuronRecipesError> { + let mut sns_neuron_recipes: Vec = vec![]; + let batch_size = 10_000_u64; + let num_calls = 100_u64; + for i in 0..num_calls { + let new_sns_neuron_recipes = self + .list_sns_neuron_recipes(agent, batch_size as u32, batch_size * i) + .await + .map_err(ListAllSnsNeuronRecipesError::CanisterCallError)?; + if new_sns_neuron_recipes.sns_neuron_recipes.is_empty() { + return Ok(sns_neuron_recipes); + } else { + sns_neuron_recipes.extend(new_sns_neuron_recipes.sns_neuron_recipes.into_iter()) + } + } + Err(ListAllSnsNeuronRecipesError::TooManyRecipes( + batch_size * num_calls, + )) + } +} diff --git a/rs/nns/governance/api/src/lib.rs b/rs/nns/governance/api/src/lib.rs index 1d8adeafcc0..3369d4770bd 100644 --- a/rs/nns/governance/api/src/lib.rs +++ b/rs/nns/governance/api/src/lib.rs @@ -2,5 +2,6 @@ pub mod bitcoin; pub mod pb; pub mod proposal_submission_helpers; pub mod proposal_validation; +mod request_impls; pub mod subnet_rental; pub mod test_api; diff --git a/rs/nns/governance/api/src/request_impls.rs b/rs/nns/governance/api/src/request_impls.rs new file mode 100644 index 00000000000..d36bf71724e --- /dev/null +++ b/rs/nns/governance/api/src/request_impls.rs @@ -0,0 +1,7 @@ +use ic_nervous_system_clients::Request; + +impl Request for crate::pb::v1::GetNeuronsFundAuditInfoRequest { + type Response = crate::pb::v1::GetNeuronsFundAuditInfoResponse; + const METHOD: &'static str = "get_neurons_fund_audit_info"; + const UPDATE: bool = false; +} diff --git a/rs/sns/swap/src/lib.rs b/rs/sns/swap/src/lib.rs index a5531dec6e5..aa342e0bcb9 100644 --- a/rs/sns/swap/src/lib.rs +++ b/rs/sns/swap/src/lib.rs @@ -4,6 +4,7 @@ pub mod logs; pub mod memory; pub mod neurons_fund; pub mod pb; +mod request_impls; pub mod swap; pub mod swap_builder; pub mod types; diff --git a/rs/sns/swap/src/request_impls.rs b/rs/sns/swap/src/request_impls.rs new file mode 100644 index 00000000000..be943eeeb08 --- /dev/null +++ b/rs/sns/swap/src/request_impls.rs @@ -0,0 +1,19 @@ +use ic_nervous_system_clients::Request; + +impl Request for crate::pb::v1::GetDerivedStateRequest { + type Response = crate::pb::v1::GetDerivedStateResponse; + const METHOD: &'static str = "get_derived_state"; + const UPDATE: bool = false; +} + +impl Request for crate::pb::v1::GetInitRequest { + type Response = crate::pb::v1::GetInitResponse; + const METHOD: &'static str = "get_init"; + const UPDATE: bool = false; +} + +impl Request for crate::pb::v1::ListSnsNeuronRecipesRequest { + type Response = crate::pb::v1::ListSnsNeuronRecipesResponse; + const METHOD: &'static str = "list_sns_neuron_recipes"; + const UPDATE: bool = false; +} From 2019e13a254f5aab0d4f6e39c522822393fed6d4 Mon Sep 17 00:00:00 2001 From: Andre Popovitch Date: Mon, 30 Sep 2024 16:07:00 -0500 Subject: [PATCH 2/2] rewrite sns-audit to use ic-nervous-system-agent --- Cargo.lock | 4 + rs/nns/governance/api/src/pb.rs | 2 + rs/sns/audit/BUILD.bazel | 8 +- rs/sns/audit/Cargo.toml | 4 + rs/sns/audit/src/lib.rs | 238 ++++++++++++-------------------- rs/sns/audit/src/main.rs | 18 ++- 6 files changed, 119 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3e3aa5b0bb..bbf398aad9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11466,10 +11466,13 @@ dependencies = [ name = "ic-sns-audit" version = "0.9.0" dependencies = [ + "anyhow", "candid", "colored", "csv", "ic-agent", + "ic-base-types", + "ic-nervous-system-agent", "ic-nervous-system-common-test-keys", "ic-neurons-fund", "ic-nns-common", @@ -11481,6 +11484,7 @@ dependencies = [ "serde", "serde_json", "textplots", + "thiserror", "tokio", ] diff --git a/rs/nns/governance/api/src/pb.rs b/rs/nns/governance/api/src/pb.rs index b07081fce94..42da3f161c4 100644 --- a/rs/nns/governance/api/src/pb.rs +++ b/rs/nns/governance/api/src/pb.rs @@ -48,6 +48,8 @@ impl fmt::Display for GovernanceError { } } +impl std::error::Error for GovernanceError {} + impl NeuronsFundEconomics { /// The default values for network economics (until we initialize it). /// Can't implement Default since it conflicts with Prost's. diff --git a/rs/sns/audit/BUILD.bazel b/rs/sns/audit/BUILD.bazel index cd5423644b5..315d6d6b79c 100644 --- a/rs/sns/audit/BUILD.bazel +++ b/rs/sns/audit/BUILD.bazel @@ -4,11 +4,13 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. + "//rs/nervous_system/agent", "//rs/nervous_system/neurons_fund", "//rs/nns/common", "//rs/nns/governance/api", "//rs/sns/governance", "//rs/sns/swap", + "//rs/types/base_types", "@crate_index//:candid", "@crate_index//:colored", "@crate_index//:csv", @@ -18,6 +20,7 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:serde_json", "@crate_index//:textplots", + "@crate_index//:thiserror", "@crate_index//:tokio", ] @@ -30,5 +33,8 @@ rust_library( rust_binary( name = "sns-audit", srcs = ["src/main.rs"], - deps = DEPENDENCIES + [":ic-sns-audit"], + deps = DEPENDENCIES + [ + ":ic-sns-audit", + "@crate_index//:anyhow", + ], ) diff --git a/rs/sns/audit/Cargo.toml b/rs/sns/audit/Cargo.toml index 737c37ff614..ad2b13de43b 100644 --- a/rs/sns/audit/Cargo.toml +++ b/rs/sns/audit/Cargo.toml @@ -15,10 +15,13 @@ name = "ic_sns_audit" path = "src/lib.rs" [dependencies] +anyhow = { workspace = true } candid = { workspace = true } colored = "2.0.0" csv = "1.1" ic-agent = { workspace = true } +ic-base-types = { path = "../../types/base_types" } +ic-nervous-system-agent = { path = "../../nervous_system/agent" } ic-nervous-system-common-test-keys = { path = "../../nervous_system/common/test_keys" } ic-neurons-fund = { path = "../../nervous_system/neurons_fund" } ic-nns-common = { path = "../../nns/common" } @@ -30,4 +33,5 @@ rust_decimal = { version = "1.25" } serde = { workspace = true } serde_json = { workspace = true } textplots = { version = "0.8" } +thiserror = { workspace = true } tokio = { workspace = true } diff --git a/rs/sns/audit/src/lib.rs b/rs/sns/audit/src/lib.rs index 98f8d31b91a..7b7b7038d3a 100644 --- a/rs/sns/audit/src/lib.rs +++ b/rs/sns/audit/src/lib.rs @@ -1,21 +1,49 @@ use std::collections::BTreeMap; -use candid::{Decode, Encode, Principal}; +use candid::Principal; use colored::{ColoredString, Colorize}; -use ic_agent::Agent; -use ic_neurons_fund::u64_to_dec; +use ic_nervous_system_agent::{ + nns, + sns::{governance::GovernanceCanister, swap::SwapCanister}, + CallCanisters, +}; use ic_nns_common::pb::v1::ProposalId; use ic_nns_governance_api::pb::v1::{ - get_neurons_fund_audit_info_response, GetNeuronsFundAuditInfoRequest, - GetNeuronsFundAuditInfoResponse, NeuronsFundAuditInfo, -}; -use ic_sns_governance::pb::v1::{GetMetadataRequest, GetMetadataResponse}; -use ic_sns_swap::pb::v1::{ - sns_neuron_recipe::Investor, GetDerivedStateRequest, GetDerivedStateResponse, GetInitRequest, - GetInitResponse, ListSnsNeuronRecipesRequest, ListSnsNeuronRecipesResponse, SnsNeuronRecipe, + get_neurons_fund_audit_info_response, GovernanceError, NeuronsFundAuditInfo, }; +use ic_sns_swap::pb::v1::sns_neuron_recipe::Investor; use rgb::RGB8; use rust_decimal::{prelude::FromPrimitive, Decimal}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AuditError { + #[error(transparent)] + CanisterCallError(#[from] CallCanistersError), + + #[error("sns was created before 1-proposal and cannot be audited using this tool; please audit this swap manually.")] + CreatedBeforeOneProposal, + + #[error("sns was created before matched funding and cannot be audited using this tool; please audit this swap manually.")] + CreatedBeforeMatchedFunding, + + #[error( + "sns was created after matched funding, but audit info is not available in NNS Governance" + )] + AuditInfoNotAvailable(#[source] GovernanceError), + + // Note: This error is not possible because `Decimal::from_u64` cannot fail. + // However, we keep the error message to protect against future changes to the `Decimal` implementation. + #[error("cannot convert value {0} to Decimal: {1}")] + DecimalConversionError(u64, String), + + #[error("swap is not in the final state yet, so `initial_neurons_fund_participation` is not specified.")] + SwapNotInFinalState, +} + +fn u64_to_dec(x: u64) -> Result> { + ic_neurons_fund::u64_to_dec(x).map_err(|s| AuditError::DecimalConversionError(x, s)) +} const GREEN: RGB8 = RGB8::new(0, 200, 30); const RED: RGB8 = RGB8::new(200, 0, 30); @@ -46,105 +74,44 @@ fn audit_check(text: &str, condition: bool) { } } -async fn list_sns_neuron_recipes( - agent: &Agent, - swap_canister_id: &Principal, -) -> Result, String> { - let mut sns_neuron_recipes: Vec = vec![]; - let batch_size = 10_000_u64; - let num_calls = 100_u64; - for i in 0..num_calls { - let response = agent - .query(swap_canister_id, "list_sns_neuron_recipes") - .with_arg( - Encode!(&ListSnsNeuronRecipesRequest { - limit: Some(batch_size as u32), - offset: Some(batch_size * i), - }) - .map_err(|e| e.to_string())?, - ) - .call() - .await - .map_err(|e| e.to_string())?; - let new_sns_neuron_recipes = Decode!(response.as_slice(), ListSnsNeuronRecipesResponse) - .map_err(|e| e.to_string())?; - if new_sns_neuron_recipes.sns_neuron_recipes.is_empty() { - return Ok(sns_neuron_recipes); - } else { - sns_neuron_recipes.extend(new_sns_neuron_recipes.sns_neuron_recipes.into_iter()) - } - } - Err(format!( - "There seem to be too many neuron recipes ({}).", - batch_size * num_calls - )) -} - -async fn validate_neurons_fund_sns_swap_participation( - agent: &Agent, - swap_canister_id: Principal, -) -> Result<(), String> { - let swap_derived_state = { - let response = agent - .query(&swap_canister_id, "get_derived_state") - .with_arg(Encode!(&GetDerivedStateRequest {}).map_err(|e| e.to_string())?) - .call() - .await - .map_err(|e| e.to_string())?; - Decode!(response.as_slice(), GetDerivedStateResponse).map_err(|e| e.to_string())? - }; - - let swap_init = { - let response = agent - .query(&swap_canister_id, "get_init") - .with_arg(Encode!(&GetInitRequest {}).map_err(|e| e.to_string())?) - .call() - .await - .map_err(|e| e.to_string())?; - let response: GetInitResponse = - Decode!(response.as_slice(), GetInitResponse).map_err(|e| e.to_string())?; - response.init.unwrap() - }; +/// Validate that the NNS (identified by `nns_url`) and an SNS instance (identified by +/// `swap_canister_id`) agree on how the SNS neurons of a successful swap have been allocated. +/// +/// This function performs a best-effort audit, e.g., there is no completeness guarantee for +/// the checks. +/// +/// Currently, the following SNS-global aspects are checked: +/// 1. Number of Neurons' Fund neurons whose maturity was initially reserved >= number of Neurons' Fund neurons who actually participated in the swap. +/// 2. Number of Neurons' Fund neurons whose maturity was initially reserved >= number of Neurons' Fund neurons who have been refunded. +/// +/// And the following neuron-local aspects are checked (only for Neurons' Fund neurons): +/// 1. initial_amount_icp_e8s == final_amount_icp_e8s + refunded_amount_icp_e8s +pub async fn validate_sns_swap( + agent: &C, + swap: SwapCanister, +) -> Result<(), AuditError> { + let swap_derived_state = swap.get_derived_state(agent).await?; + + let swap_init = swap.get_init(agent).await?.init.unwrap(); + let governance = GovernanceCanister::new( + Principal::from_text(swap_init.sns_governance_canister_id.clone()).unwrap(), + ); - let sns_governance_canister_id = swap_init.sns_governance_canister_id.clone(); - let sns_governance_canister_id = Principal::from_text(sns_governance_canister_id).unwrap(); - - let metadata = { - let response = agent - .query(&sns_governance_canister_id, "get_metadata") - .with_arg(Encode!(&GetMetadataRequest {}).map_err(|e| e.to_string())?) - .call() - .await - .map_err(|e| e.to_string())?; - Decode!(response.as_slice(), GetMetadataResponse).map_err(|e| e.to_string())? - }; + let metadata = governance.metadata(agent).await?; let sns_name = metadata.name.unwrap(); + println!("sns_name = {}", sns_name); - let nns_governance_canister_id = swap_init.nns_governance_canister_id.clone(); - let nns_governance_canister_id = Principal::from_text(nns_governance_canister_id).unwrap(); let Some(nns_proposal_id) = swap_init.nns_proposal_id.as_ref() else { - return Err(format!( - "{} swap has been created before 1-proposal and cannot be audited using this tool; please \ - audit this swap manually.", - sns_name, - )); - }; - let audit_info = { - let response = agent - .query(&nns_governance_canister_id, "get_neurons_fund_audit_info") - .with_arg( - Encode!(&GetNeuronsFundAuditInfoRequest { - nns_proposal_id: Some(ProposalId { - id: *nns_proposal_id - }), - }) - .map_err(|e| e.to_string())?, - ) - .call() - .await - .map_err(|e| e.to_string())?; - Decode!(response.as_slice(), GetNeuronsFundAuditInfoResponse).map_err(|e| e.to_string())? + return Err(AuditError::CreatedBeforeOneProposal); }; + let audit_info = nns::governance::get_neurons_fund_audit_info( + agent, + ProposalId { + id: *nns_proposal_id, + }, + ) + .await?; + let audit_info = match audit_info.result.clone().unwrap() { get_neurons_fund_audit_info_response::Result::Ok( get_neurons_fund_audit_info_response::Ok { @@ -154,16 +121,9 @@ async fn validate_neurons_fund_sns_swap_participation( get_neurons_fund_audit_info_response::Result::Err(err) => { if err.error_message.starts_with("Neurons Fund data not found") { - return Err(format!( - "{} swap has been created before Matched Funding and cannot be audited using this \ - tool; please audit this swap manually.", - sns_name, - )); + return Err(AuditError::CreatedBeforeMatchedFunding); } else { - return Err(format!( - "Expected GetNeuronsFundAuditInfoResponse for {} to be Ok, got {:?}", - sns_name, audit_info, - )); + return Err(AuditError::AuditInfoNotAvailable(err)); } } }; @@ -187,10 +147,12 @@ async fn validate_neurons_fund_sns_swap_participation( swap_init.neuron_basket_construction_parameters.unwrap(); let buyer_total_icp_e8s = swap_derived_state.buyer_total_icp_e8s.unwrap(); let sns_token_e8s = swap_init.sns_token_e8s.unwrap(); - let sns_tokens_per_icp = u64_to_dec(sns_token_e8s)? / u64_to_dec(buyer_total_icp_e8s)?; + let sns_tokens_per_icp = + u64_to_dec::(sns_token_e8s)? / u64_to_dec::(buyer_total_icp_e8s)?; println!("sns_tokens_per_icp = {:?}", sns_tokens_per_icp); - let sns_neuron_recipes: Vec<_> = list_sns_neuron_recipes(agent, &swap_canister_id) + let sns_neuron_recipes: Vec<_> = swap + .list_all_sns_neuron_recipes(agent) .await .unwrap() .into_iter() @@ -205,9 +167,9 @@ async fn validate_neurons_fund_sns_swap_participation( }) .collect(); - let neurons_fund_refunds = audit_info.neurons_fund_refunds.ok_or_else(|| { - format!("SNS swap {} is not in the final state yet, so `neurons_fund_refunds` is not specified.", sns_name) - })?; + let neurons_fund_refunds = audit_info + .neurons_fund_refunds + .ok_or(AuditError::SwapNotInFinalState)?; let refunded_neuron_portions = neurons_fund_refunds.neurons_fund_neuron_portions; let mut refunded_amounts_per_controller = BTreeMap::new(); for refunded_neuron_portion in refunded_neuron_portions.iter() { @@ -219,9 +181,9 @@ async fn validate_neurons_fund_sns_swap_participation( .or_insert(new_amount_icp_e8s); } - let initial_neurons_fund_participation = audit_info.initial_neurons_fund_participation.ok_or_else(|| { - format!("SNS swap {} is not in the final state yet, so `initial_neurons_fund_participation` is not specified.", sns_name) - })?; + let initial_neurons_fund_participation = audit_info + .initial_neurons_fund_participation + .ok_or(AuditError::SwapNotInFinalState)?; let initial_neuron_portions = initial_neurons_fund_participation .neurons_fund_reserves .unwrap() @@ -236,9 +198,9 @@ async fn validate_neurons_fund_sns_swap_participation( .or_insert(new_amount_icp_e8s); } - let final_neurons_fund_participation = audit_info.final_neurons_fund_participation.ok_or_else(|| { - format!("SNS swap {} is not in the final state yet, so `final_neurons_fund_participation` is not specified.", sns_name) - })?; + let final_neurons_fund_participation = audit_info + .final_neurons_fund_participation + .ok_or(AuditError::SwapNotInFinalState)?; let final_neuron_portions = final_neurons_fund_participation .neurons_fund_reserves .unwrap() @@ -325,12 +287,12 @@ async fn validate_neurons_fund_sns_swap_participation( for (controller, nns_neurons) in investment_per_controller_icp_e8s.iter() { let amount_icp_e8s = nns_neurons.iter().sum::(); - let amount_icp_e8s = u64_to_dec(amount_icp_e8s)?; + let amount_icp_e8s = u64_to_dec::(amount_icp_e8s)?; let sns_neurons = sns_neuron_recipes_per_controller .get(controller) .expect("All Neuron's Fund participants should have SNS neuron recipes."); let amount_sns_e8s = sns_neurons.iter().sum::(); - let amount_sns_e8s = u64_to_dec(amount_sns_e8s)?; + let amount_sns_e8s = u64_to_dec::(amount_sns_e8s)?; let absolute_error_sns_e8s = (amount_icp_e8s * sns_tokens_per_icp - amount_sns_e8s).abs(); let error_per_cent = (Decimal::new(100, 0) * absolute_error_sns_e8s) / amount_sns_e8s; let nns_neurons_str = nns_neurons @@ -354,27 +316,3 @@ async fn validate_neurons_fund_sns_swap_participation( Ok(()) } - -/// Validate that the NNS (identified by `nns_url`) and an SNS instance (identified by -/// `swap_canister_id`) agree on how the SNS neurons of a successful swap have been allocated. -/// -/// This function performs a best-effort audit, e.g., there is no completeness guarantee for -/// the checks. -/// -/// Currently, the following SNS-global aspects are checked: -/// 1. Number of Neurons' Fund neurons whose maturity was initially reserved >= number of Neurons' Fund neurons who actually participated in the swap. -/// 2. Number of Neurons' Fund neurons whose maturity was initially reserved >= number of Neurons' Fund neurons who have been refunded. -/// -/// And the following neuron-local aspects are checked (only for Neurons' Fund neurons): -/// 1. initial_amount_icp_e8s == final_amount_icp_e8s + refunded_amount_icp_e8s -pub async fn validate_sns_swap(nns_url: &str, swap_canister_id: Principal) -> Result<(), String> { - let agent = Agent::builder() - .with_url(nns_url) - .with_verify_query_signatures(false) - .build() - .map_err(|e| e.to_string())?; - - validate_neurons_fund_sns_swap_participation(&agent, swap_canister_id).await?; - - Ok(()) -} diff --git a/rs/sns/audit/src/main.rs b/rs/sns/audit/src/main.rs index d2be859b93d..0ee4c4dc1ce 100644 --- a/rs/sns/audit/src/main.rs +++ b/rs/sns/audit/src/main.rs @@ -1,14 +1,24 @@ +use anyhow::bail; use candid::Principal; +use ic_agent::Agent; +use ic_nervous_system_agent::sns::swap::SwapCanister; use ic_sns_audit::validate_sns_swap; #[tokio::main] -async fn main() -> Result<(), String> { +async fn main() -> anyhow::Result<()> { let args: Vec<_> = std::env::args().collect(); if args.len() != 3 { - return Err("Please specify NNS_URL and SWAP_CANISTER_ID as CLI arguments.".to_string()); + bail!("Please specify NNS_URL and SWAP_CANISTER_ID as CLI arguments."); } let nns_url = &args[1]; let swap_canister_id = &args[2]; - let swap_canister_id = Principal::from_text(swap_canister_id).unwrap(); - validate_sns_swap(nns_url, swap_canister_id).await + let swap = SwapCanister::new(Principal::from_text(swap_canister_id).unwrap()); + + let agent = Agent::builder() + .with_url(nns_url) + .with_verify_query_signatures(false) + .build()?; + + validate_sns_swap(&agent, swap).await?; + Ok(()) }