1use super::{
8 SnsMaturityDisbursementRow, SnsNeuronPermissionRow,
9 detail::{
10 SnsPolicyObservationStatus, neuron_policy_observations, permission_code_is_assessable,
11 permission_code_policy_observations, validate_canonical_principal,
12 validate_maturity_disbursements, validate_neuron_permissions,
13 },
14};
15use crate::{
16 hex::is_lowercase_hex_32_bytes,
17 report::ReportDataSource,
18 sns::report::{
19 MAINNET_SNS_WASM_CANISTER_ID, MainnetSnsCanisters, SNS_REWARD_CHECKPOINT_MAX_NEURONS,
20 SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION,
21 model::reports::{SnsGovernanceParameters, SnsRunningVersionResponse},
22 },
23 subnet_catalog::MAINNET_NETWORK,
24};
25use ic_platform::candid::contract::{CandidType, serde::Deserialize};
26use serde::Serialize;
27use std::collections::HashSet;
28use thiserror::Error as ThisError;
29
30#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
37#[candid_path("ic_platform::candid::contract")]
38#[serde(deny_unknown_fields)]
39pub struct SnsRewardProposalId {
40 pub id: u64,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq, CandidType, Deserialize, Serialize)]
51#[candid_path("ic_platform::candid::contract")]
52#[serde(deny_unknown_fields)]
53pub struct SnsRewardEvent {
54 pub rounds_since_last_distribution: Option<u64>,
56 pub actual_timestamp_seconds: u64,
58 pub end_timestamp_seconds: Option<u64>,
60 pub total_available_e8s_equivalent: Option<u64>,
62 pub distributed_e8s_equivalent: u64,
64 pub round: u64,
66 pub settled_proposals: Vec<SnsRewardProposalId>,
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
77#[serde(rename_all = "snake_case")]
78pub enum SnsRewardCollectionStatus {
79 ApiExhaustedObserved,
81}
82
83impl SnsRewardCollectionStatus {
84 #[must_use]
86 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::ApiExhaustedObserved => "api_exhausted_observed",
89 }
90 }
91}
92
93#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
100#[serde(deny_unknown_fields)]
101pub struct SnsRewardCheckpointRow {
102 pub neuron_id: String,
104 pub created_timestamp_seconds: u64,
106 pub maturity_e8s_equivalent: u64,
108 pub staked_maturity_e8s_equivalent: Option<u64>,
110 pub auto_stake_maturity: Option<bool>,
112 pub permissions: Vec<SnsNeuronPermissionRow>,
114 pub disburse_maturity_in_progress: Vec<SnsMaturityDisbursementRow>,
116}
117
118impl SnsRewardCheckpointRow {
119 #[must_use]
121 pub fn checked_combined_maturity(&self) -> Option<u64> {
122 self.maturity_e8s_equivalent
123 .checked_add(self.staked_maturity_e8s_equivalent.unwrap_or(0))
124 }
125
126 #[must_use]
128 pub fn derived_policy_observations(
129 &self,
130 ) -> (SnsPolicyObservationStatus, SnsPolicyObservationStatus) {
131 neuron_policy_observations(
132 &self.permissions,
133 !self.disburse_maturity_in_progress.is_empty(),
134 )
135 }
136}
137
138#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
145#[serde(deny_unknown_fields)]
146pub struct SnsRewardCheckpointReport {
147 pub schema_version: u32,
149 pub network: String,
151 pub sns_wasm_canister_id: String,
153 pub source_endpoint: String,
155 pub fetched_by: String,
157 pub id: usize,
159 pub name: String,
161 #[serde(flatten)]
163 pub canisters: MainnetSnsCanisters,
164 pub data_source: ReportDataSource,
166 pub collection_started_at_unix_secs: u64,
168 pub collection_completed_at_unix_secs: u64,
170 pub page_count: u32,
172 pub collection_status: SnsRewardCollectionStatus,
174 pub point_in_time_guaranteed: bool,
176 pub parameters_before: SnsGovernanceParameters,
178 pub parameters_after: SnsGovernanceParameters,
180 pub reward_event_before: SnsRewardEvent,
182 pub reward_event_after: SnsRewardEvent,
184 pub running_version_before: SnsRunningVersionResponse,
186 pub running_version_after: SnsRunningVersionResponse,
188 pub rows: Vec<SnsRewardCheckpointRow>,
190}
191
192#[derive(Debug, Eq, PartialEq, ThisError)]
199#[error("invalid SNS reward checkpoint: {reason}")]
200pub struct SnsRewardCheckpointValidationError {
201 pub reason: String,
203}
204
205pub fn validate_sns_reward_checkpoint_report(
207 report: &SnsRewardCheckpointReport,
208) -> Result<(), SnsRewardCheckpointValidationError> {
209 validate_checkpoint_evidence(report)?;
210 report.summary().map(|_| ())
211}
212
213pub(in crate::sns::report) enum RewardCheckpointAssessment {
220 Valid(SnsRewardCheckpointSummary),
221 Invalid {
222 error: SnsRewardCheckpointValidationError,
223 summary: Option<SnsRewardCheckpointSummary>,
224 },
225}
226
227impl RewardCheckpointAssessment {
228 pub(in crate::sns::report) const fn summary(&self) -> Option<&SnsRewardCheckpointSummary> {
229 match self {
230 Self::Valid(summary) => Some(summary),
231 Self::Invalid { summary, .. } => summary.as_ref(),
232 }
233 }
234}
235
236pub(in crate::sns::report) fn assess_reward_checkpoint(
238 report: &SnsRewardCheckpointReport,
239) -> RewardCheckpointAssessment {
240 let evidence = validate_checkpoint_evidence(report);
241 match report.summary() {
242 Ok(summary) => match evidence {
243 Ok(()) => RewardCheckpointAssessment::Valid(summary),
244 Err(error) => RewardCheckpointAssessment::Invalid {
245 error,
246 summary: Some(summary),
247 },
248 },
249 Err(reason) => RewardCheckpointAssessment::Invalid {
250 error: evidence.err().unwrap_or(reason),
251 summary: None,
252 },
253 }
254}
255
256impl SnsRewardCheckpointReport {
257 pub fn summary(
259 &self,
260 ) -> Result<SnsRewardCheckpointSummary, SnsRewardCheckpointValidationError> {
261 recompute_reward_checkpoint_summary(&self.parameters_before, &self.rows)
262 .map_err(invalid_validation)
263 }
264
265 #[must_use]
267 pub const fn row_count(&self) -> usize {
268 self.rows.len()
269 }
270
271 #[must_use]
273 pub fn collection_row_ceiling(&self) -> Option<u64> {
274 reward_checkpoint_neuron_ceiling(&self.parameters_before)
275 }
276
277 #[must_use]
279 pub const fn client_query_count(&self) -> Option<u32> {
280 self.page_count.checked_add(8)
281 }
282}
283
284fn validate_checkpoint_evidence(
285 report: &SnsRewardCheckpointReport,
286) -> Result<(), SnsRewardCheckpointValidationError> {
287 validate_checkpoint_header(report)?;
288 validate_checkpoint_brackets(report)?;
289 validate_checkpoint_rows(&report.rows, report.collection_completed_at_unix_secs)
290}
291
292fn validate_checkpoint_header(
293 report: &SnsRewardCheckpointReport,
294) -> Result<(), SnsRewardCheckpointValidationError> {
295 if report.schema_version != SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION {
296 return Err(invalid_validation(format!(
297 "schema version {} does not equal {}",
298 report.schema_version, SNS_REWARD_CHECKPOINT_REPORT_SCHEMA_VERSION
299 )));
300 }
301 for (field, principal) in [
302 ("sns_wasm_canister_id", report.sns_wasm_canister_id.as_str()),
303 (
304 "root_canister_id",
305 report.canisters.root_canister_id.as_str(),
306 ),
307 (
308 "governance_canister_id",
309 report.canisters.governance_canister_id.as_str(),
310 ),
311 (
312 "ledger_canister_id",
313 report.canisters.ledger_canister_id.as_str(),
314 ),
315 (
316 "swap_canister_id",
317 report.canisters.swap_canister_id.as_str(),
318 ),
319 (
320 "index_canister_id",
321 report.canisters.index_canister_id.as_str(),
322 ),
323 ] {
324 validate_canonical_principal(principal, field).map_err(invalid_validation)?;
325 }
326 let canister_ids = [
327 report.sns_wasm_canister_id.as_str(),
328 report.canisters.root_canister_id.as_str(),
329 report.canisters.governance_canister_id.as_str(),
330 report.canisters.ledger_canister_id.as_str(),
331 report.canisters.swap_canister_id.as_str(),
332 report.canisters.index_canister_id.as_str(),
333 ];
334 if canister_ids.into_iter().collect::<HashSet<_>>().len() != canister_ids.len() {
335 return Err(invalid_validation(
336 "checkpoint canister roles do not contain unique principals",
337 ));
338 }
339 if report.sns_wasm_canister_id != MAINNET_SNS_WASM_CANISTER_ID {
340 return Err(invalid_validation("unexpected SNS-W canister identity"));
341 }
342 if report.network != MAINNET_NETWORK {
343 return Err(invalid_validation("checkpoint network must be ic"));
344 }
345 if report.source_endpoint.is_empty() || report.fetched_by.is_empty() {
346 return Err(invalid_validation(
347 "checkpoint source_endpoint and fetched_by must be non-empty",
348 ));
349 }
350 if report.id == 0 || report.name.is_empty() {
351 return Err(invalid_validation(
352 "checkpoint SNS list id and display name must be non-empty",
353 ));
354 }
355 if report.data_source != ReportDataSource::Live {
356 return Err(invalid_validation("checkpoint data_source must be live"));
357 }
358 if report.point_in_time_guaranteed {
359 return Err(invalid_validation(
360 "checkpoint cannot claim a point-in-time neuron snapshot",
361 ));
362 }
363 validate_checkpoint_collection_metadata(report)
364}
365
366fn validate_checkpoint_collection_metadata(
367 report: &SnsRewardCheckpointReport,
368) -> Result<(), SnsRewardCheckpointValidationError> {
369 if report.collection_completed_at_unix_secs < report.collection_started_at_unix_secs {
370 return Err(invalid_validation(
371 "collection completion precedes collection start",
372 ));
373 }
374 if report.page_count == 0 {
375 return Err(invalid_validation(
376 "checkpoint must contain at least one page",
377 ));
378 }
379 report
380 .client_query_count()
381 .ok_or_else(|| invalid_validation("page_count + 8 exceeds the native u32 contract"))?;
382 let ceiling = report
383 .collection_row_ceiling()
384 .ok_or_else(|| invalid_validation("invalid max_number_of_neurons"))?;
385 if u64::try_from(report.rows.len())
386 .ok()
387 .is_none_or(|rows| rows > ceiling)
388 {
389 return Err(invalid_validation(
390 "raw row count exceeds the mandatory collection ceiling",
391 ));
392 }
393 Ok(())
394}
395
396pub(in crate::sns::report) fn reward_checkpoint_neuron_ceiling(
397 parameters: &SnsGovernanceParameters,
398) -> Option<u64> {
399 parameters
400 .max_number_of_neurons
401 .filter(|value| (1..=SNS_REWARD_CHECKPOINT_MAX_NEURONS).contains(value))
402}
403
404pub(in crate::sns::report) fn reward_checkpoint_bracket_mismatch(
406 parameters: (&SnsGovernanceParameters, &SnsGovernanceParameters),
407 events: (&SnsRewardEvent, &SnsRewardEvent),
408 versions: (&SnsRunningVersionResponse, &SnsRunningVersionResponse),
409) -> Option<(&'static str, &'static str)> {
410 [
411 (
412 "nervous-system parameters",
413 "nervous-system parameter brackets differ",
414 parameters.0 == parameters.1,
415 ),
416 (
417 "reward event",
418 "reward-event brackets differ",
419 events.0 == events.1,
420 ),
421 (
422 "running SNS version",
423 "running-version brackets differ",
424 versions.0 == versions.1,
425 ),
426 ]
427 .into_iter()
428 .find_map(|(component, reason, stable)| (!stable).then_some((component, reason)))
429}
430
431fn validate_checkpoint_brackets(
432 report: &SnsRewardCheckpointReport,
433) -> Result<(), SnsRewardCheckpointValidationError> {
434 if let Some((_, reason)) = reward_checkpoint_bracket_mismatch(
435 (&report.parameters_before, &report.parameters_after),
436 (&report.reward_event_before, &report.reward_event_after),
437 (
438 &report.running_version_before,
439 &report.running_version_after,
440 ),
441 ) {
442 return Err(invalid_validation(reason));
443 }
444 validate_sns_reward_event_evidence(&report.reward_event_after).map_err(invalid_validation)?;
445 validate_reward_event_position(report)?;
446 validate_sns_reward_running_version_evidence(&report.running_version_after)
447 .map_err(invalid_validation)?;
448 validate_sns_reward_checkpoint_parameter_evidence(&report.parameters_after)
449 .map_err(invalid_validation)
450}
451
452fn validate_reward_event_position(
453 report: &SnsRewardCheckpointReport,
454) -> Result<(), SnsRewardCheckpointValidationError> {
455 let event = &report.reward_event_after;
456 if event.actual_timestamp_seconds > report.collection_completed_at_unix_secs
457 || event
458 .end_timestamp_seconds
459 .is_none_or(|end| end > report.collection_completed_at_unix_secs)
460 {
461 return Err(invalid_validation(
462 "reward event timestamps exceed checkpoint collection completion",
463 ));
464 }
465 Ok(())
466}
467
468fn validate_checkpoint_rows(
469 rows: &[SnsRewardCheckpointRow],
470 collection_completed_at_unix_secs: u64,
471) -> Result<(), SnsRewardCheckpointValidationError> {
472 let mut previous_id: Option<&str> = None;
473 for row in rows {
474 if !is_lowercase_hex_32_bytes(&row.neuron_id) {
475 return Err(invalid_validation(format!(
476 "neuron id {} is not 32-byte lowercase hexadecimal text",
477 row.neuron_id
478 )));
479 }
480 if previous_id.is_some_and(|previous| previous >= row.neuron_id.as_str()) {
481 return Err(invalid_validation(
482 "checkpoint neuron ids are not strictly increasing",
483 ));
484 }
485 previous_id = Some(&row.neuron_id);
486 if row.created_timestamp_seconds > collection_completed_at_unix_secs {
487 return Err(invalid_validation(format!(
488 "neuron {} creation timestamp exceeds collection completion",
489 row.neuron_id
490 )));
491 }
492 validate_neuron_permissions(&row.permissions, false)
493 .map_err(|reason| invalid_validation(format!("neuron {}: {reason}", row.neuron_id)))?;
494 validate_maturity_disbursements(&row.disburse_maturity_in_progress)
495 .map_err(invalid_validation)?;
496 }
497 Ok(())
498}
499
500pub(in crate::sns::report) fn validate_sns_reward_running_version_evidence(
501 response: &SnsRunningVersionResponse,
502) -> Result<(), String> {
503 let deployed = response
504 .deployed_version
505 .as_ref()
506 .ok_or_else(|| "running version has no deployed_version".to_string())?;
507 deployed.validate_hashes("deployed_version", Some(32))?;
508 if let Some(target) = response
509 .pending_version
510 .as_ref()
511 .and_then(|pending| pending.target_version.as_ref())
512 {
513 target.validate_hashes("pending_version.target_version", Some(32))?;
514 }
515 Ok(())
516}
517
518pub(in crate::sns::report) fn validate_sns_reward_checkpoint_parameter_evidence(
519 parameters: &SnsGovernanceParameters,
520) -> Result<(), String> {
521 validate_default_followees(parameters)?;
522 validate_parameter_permissions(parameters)
523}
524
525fn validate_default_followees(parameters: &SnsGovernanceParameters) -> Result<(), String> {
526 let Some(defaults) = parameters.default_followees.as_ref() else {
527 return Ok(());
528 };
529 let mut function_ids = HashSet::new();
530 for row in &defaults.followees {
531 if !function_ids.insert(row.function_id) {
532 return Err("default followees contain a duplicate function id".to_string());
533 }
534 let mut neuron_ids = HashSet::new();
535 for id in &row.followee_neuron_ids {
536 if !is_lowercase_hex_32_bytes(id) {
537 return Err("default followees contain a non-canonical neuron id".to_string());
538 }
539 if !neuron_ids.insert(id) {
540 return Err("default followees contain a duplicate neuron id".to_string());
541 }
542 }
543 }
544 Ok(())
545}
546
547fn validate_parameter_permissions(parameters: &SnsGovernanceParameters) -> Result<(), String> {
548 for (field, permissions) in [
549 (
550 "neuron_claimer_permissions",
551 parameters.neuron_claimer_permissions.as_ref(),
552 ),
553 (
554 "neuron_grantable_permissions",
555 parameters.neuron_grantable_permissions.as_ref(),
556 ),
557 ] {
558 let Some(permissions) = permissions else {
559 continue;
560 };
561 let mut codes = HashSet::new();
562 if permissions
563 .permissions
564 .iter()
565 .any(|code| !codes.insert(*code))
566 {
567 return Err(format!("{field} contains a duplicate permission code"));
568 }
569 }
570 Ok(())
571}
572
573pub(in crate::sns::report) fn validate_sns_reward_event_evidence(
574 event: &SnsRewardEvent,
575) -> Result<(), String> {
576 if event.end_timestamp_seconds.is_none() {
577 return Err("reward event is missing canonical end_timestamp_seconds".to_string());
578 }
579 let mut proposal_ids = HashSet::new();
580 if event
581 .settled_proposals
582 .iter()
583 .any(|proposal| !proposal_ids.insert(proposal.id))
584 {
585 return Err("reward event contains duplicate settled proposal ids".to_string());
586 }
587 Ok(())
588}
589
590fn invalid_validation(reason: impl Into<String>) -> SnsRewardCheckpointValidationError {
591 SnsRewardCheckpointValidationError {
592 reason: reason.into(),
593 }
594}
595
596#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
603#[serde(deny_unknown_fields)]
604pub struct SnsRewardCheckpointSummary {
605 pub aggregate_maturity_e8s_equivalent: u64,
607 pub aggregate_staked_maturity_e8s_equivalent: u64,
609 pub aggregate_combined_maturity_e8s_equivalent: u64,
611 pub permission_entry_count: usize,
613 pub unassessable_permission_code_count: usize,
615 pub pending_maturity_disbursement_count: usize,
617 pub auto_stake_maturity_enabled_count: usize,
619 pub auto_stake_maturity_disabled_count: usize,
621 pub auto_stake_maturity_unspecified_count: usize,
623 pub manage_principals_grantable: Option<bool>,
625 pub maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus,
627 pub manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus,
629 pub maturity_conversion_policy_observed_status: SnsPolicyObservationStatus,
631}
632
633pub(in crate::sns::report) fn recompute_reward_checkpoint_summary(
634 parameters: &SnsGovernanceParameters,
635 rows: &[SnsRewardCheckpointRow],
636) -> Result<SnsRewardCheckpointSummary, String> {
637 let mut summary = SnsRewardCheckpointSummary {
638 aggregate_maturity_e8s_equivalent: 0,
639 aggregate_staked_maturity_e8s_equivalent: 0,
640 aggregate_combined_maturity_e8s_equivalent: 0,
641 permission_entry_count: 0,
642 unassessable_permission_code_count: 0,
643 pending_maturity_disbursement_count: 0,
644 auto_stake_maturity_enabled_count: 0,
645 auto_stake_maturity_disabled_count: 0,
646 auto_stake_maturity_unspecified_count: 0,
647 manage_principals_grantable: parameters
648 .neuron_grantable_permissions
649 .as_ref()
650 .map(|permissions| permissions.permissions.contains(&2)),
651 maturity_mint_conversion_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
652 manual_maturity_staking_observed_disabled: SnsPolicyObservationStatus::ObservedSatisfied,
653 maturity_conversion_policy_observed_status: SnsPolicyObservationStatus::ObservedSatisfied,
654 };
655 let (parameter_mint, parameter_staking, parameter_unknown_count) =
656 parameter_policy_observations(parameters);
657 summary.maturity_mint_conversion_observed_disabled = parameter_mint;
658 summary.manual_maturity_staking_observed_disabled = parameter_staking;
659 summary.unassessable_permission_code_count = parameter_unknown_count;
660
661 for row in rows {
662 accumulate_reward_row(&mut summary, row)?;
663 }
664 summary.maturity_conversion_policy_observed_status = summary
665 .maturity_mint_conversion_observed_disabled
666 .combine(summary.manual_maturity_staking_observed_disabled);
667 Ok(summary)
668}
669
670fn accumulate_reward_row(
671 summary: &mut SnsRewardCheckpointSummary,
672 row: &SnsRewardCheckpointRow,
673) -> Result<(), String> {
674 let combined = row.checked_combined_maturity().ok_or_else(|| {
675 format!(
676 "neuron {} combined maturity exceeds the native u64 contract",
677 row.neuron_id
678 )
679 })?;
680 let (row_mint, row_staking) = row.derived_policy_observations();
681 summary.aggregate_maturity_e8s_equivalent = checked_sum(
682 summary.aggregate_maturity_e8s_equivalent,
683 row.maturity_e8s_equivalent,
684 "aggregate maturity",
685 )?;
686 summary.aggregate_staked_maturity_e8s_equivalent = checked_sum(
687 summary.aggregate_staked_maturity_e8s_equivalent,
688 row.staked_maturity_e8s_equivalent.unwrap_or(0),
689 "aggregate staked maturity",
690 )?;
691 summary.aggregate_combined_maturity_e8s_equivalent = checked_sum(
692 summary.aggregate_combined_maturity_e8s_equivalent,
693 combined,
694 "aggregate combined maturity",
695 )?;
696 accumulate_reward_row_counts(summary, row)?;
697 summary.maturity_mint_conversion_observed_disabled = summary
698 .maturity_mint_conversion_observed_disabled
699 .combine(row_mint);
700 summary.manual_maturity_staking_observed_disabled = summary
701 .manual_maturity_staking_observed_disabled
702 .combine(row_staking);
703 Ok(())
704}
705
706fn accumulate_reward_row_counts(
707 summary: &mut SnsRewardCheckpointSummary,
708 row: &SnsRewardCheckpointRow,
709) -> Result<(), String> {
710 summary.permission_entry_count = summary
711 .permission_entry_count
712 .checked_add(row.permissions.len())
713 .ok_or_else(|| "permission entry count overflow".to_string())?;
714 summary.pending_maturity_disbursement_count = summary
715 .pending_maturity_disbursement_count
716 .checked_add(row.disburse_maturity_in_progress.len())
717 .ok_or_else(|| "pending maturity disbursement count overflow".to_string())?;
718 summary.unassessable_permission_code_count = summary
719 .unassessable_permission_code_count
720 .checked_add(
721 row.permissions
722 .iter()
723 .flat_map(|permission| permission.permission_types.iter())
724 .filter(|value| !permission_code_is_assessable(**value))
725 .count(),
726 )
727 .ok_or_else(|| "unassessable permission code count overflow".to_string())?;
728 match row.auto_stake_maturity {
729 Some(true) => increment(&mut summary.auto_stake_maturity_enabled_count)?,
730 Some(false) => increment(&mut summary.auto_stake_maturity_disabled_count)?,
731 None => increment(&mut summary.auto_stake_maturity_unspecified_count)?,
732 }
733 Ok(())
734}
735
736fn parameter_policy_observations(
737 parameters: &SnsGovernanceParameters,
738) -> (
739 SnsPolicyObservationStatus,
740 SnsPolicyObservationStatus,
741 usize,
742) {
743 let mut mint = SnsPolicyObservationStatus::ObservedSatisfied;
744 let mut staking = SnsPolicyObservationStatus::ObservedSatisfied;
745 let mut unknown_count = 0;
746 for permissions in [
747 parameters.neuron_claimer_permissions.as_ref(),
748 parameters.neuron_grantable_permissions.as_ref(),
749 ] {
750 let Some(permissions) = permissions else {
751 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
752 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
753 continue;
754 };
755 if permissions.permissions.is_empty() {
756 mint = mint.combine(SnsPolicyObservationStatus::Unassessable);
757 staking = staking.combine(SnsPolicyObservationStatus::Unassessable);
758 }
759 for code in &permissions.permissions {
760 let (code_mint, code_staking) = permission_code_policy_observations(*code);
761 mint = mint.combine(code_mint);
762 staking = staking.combine(code_staking);
763 if !permission_code_is_assessable(*code) {
764 unknown_count += 1;
765 }
766 }
767 }
768 (mint, staking, unknown_count)
769}
770
771fn checked_sum(left: u64, right: u64, field: &str) -> Result<u64, String> {
772 left.checked_add(right)
773 .ok_or_else(|| format!("{field} exceeds the native u64 contract"))
774}
775
776fn increment(value: &mut usize) -> Result<(), String> {
777 *value = value
778 .checked_add(1)
779 .ok_or_else(|| "checkpoint count overflow".to_string())?;
780 Ok(())
781}