// file: kb-lib/src/materializer/risk/core.rs // version: 6 //! Stable fact-only risk projections for committed SPL Token and ATA observations. const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ crate::MdEventFamily::TokenAccount, crate::MdEventFamily::TokenMint, crate::MdEventFamily::Admin, crate::MdEventFamily::Lifecycle, crate::MdEventFamily::Audit, ]; const RISK_ENTRIES: &[&str] = &[ "approve", "approve_checked", "revoke", "freeze_account", "thaw_account", "set_authority", "initialize_multisig", "initialize_multisig2", "initialize_default_account_state", "update_default_account_state", "enable_required_transfer_memos", "disable_required_transfer_memos", "enable_cpi_guard", "disable_cpi_guard", "initialize_non_transferable_mint", "initialize_pausable_config", "pause", "resume", "initialize_permissioned_burn", "enable_confidential_credits", "disable_confidential_credits", "enable_non_confidential_credits", "disable_non_confidential_credits", ]; /// Stable fact-only classic SPL Token risk materializer. #[derive(Clone, Debug, Default)] pub struct MtRiskMaterializer; impl crate::MtMaterializer for crate::MtRiskMaterializer { fn materializer_name(&self) -> &'static str { return "kb-lib.materializer.risk"; } fn materializer_version(&self) -> &'static str { return env!("CARGO_PKG_VERSION"); } fn accepts_event(&self, event: &crate::MdDecodedProtocolEvent) -> bool { return accepts_event(event); } fn materialize_event( &self, _event: &crate::MdDecodedProtocolEvent, ) -> kb_core::Result> { return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtRiskMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { name: crate::MT_RISK_PROCESSOR_NAME.to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { return ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool { return accepts_event(&observation.event); } fn transaction_policy( &self, _family: crate::MdEventFamily, ) -> crate::MtApiMaterializationTransactionPolicy { return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; } fn materialize( &self, observation: &crate::DcApiDecodedObservation, ) -> crate::MtApiMaterializerExecutionResult { if !accepts_event(&observation.event) { return crate::MtApiMaterializerExecutionResult::ignored(); } if observation.transaction_failed || !observation.observation_committed { return crate::MtApiMaterializerExecutionResult::refused( "uncommitted_token_risk_fact_refused", "failed or uncommitted SPL Token or ATA observations cannot create risk facts", ); } let ata = is_ata_recovery(&observation.event); if ata && !ata_addresses_valid(&observation.payload_json) { return crate::MtApiMaterializerExecutionResult::refused( "invalid_nested_ata_risk_fact_refused", "nested ATA risk fact requires all three canonical address validations", ); } let operation = observation.event.event_name.0.as_str(); let parameters = observation .payload_json .get("parameters") .cloned() .unwrap_or(serde_json::Value::Null); let accounts = observation .payload_json .get("accounts") .cloned() .unwrap_or(serde_json::Value::Null); let authority = observation .payload_json .get("authority") .cloned() .unwrap_or(serde_json::Value::Null); let addresses = observation .payload_json .get("addresses") .cloned() .unwrap_or(serde_json::Value::Null); let risk_kind = match risk_kind(operation, ¶meters, &accounts) { std::option::Option::Some(value) => value, std::option::Option::None => { return crate::MtApiMaterializerExecutionResult::ignored(); }, }; let output = crate::MtApiMaterializedOutput { output_key: if ata { format!("ata_risk_fact:{risk_kind}:0") } else { format!("spl_token_risk_fact:{risk_kind}:0") }, family: crate::MdMaterializedEventFamily::Risk, payload_json: serde_json::json!({ "projectionVersion": crate::MT_RISK_PROJECTION_VERSION, "domain": if ata { "spl_associated_token_account_risk_fact" } else { "spl_token_risk_fact" }, "riskKind": risk_kind, "operation": operation, "signature": observation.event.signature.0.clone(), "slot": observation.event.slot.0, "instructionPath": observation.event.instruction_path.0.clone(), "programId": observation.event.program_id.0.clone(), "parameters": parameters, "accounts": accounts, "authority": authority, "addresses": addresses, "interpretation": { "score": serde_json::Value::Null, "severity": serde_json::Value::Null, "factOnly": true }, "committed": true, "provenance": { "processorName": crate::MT_RISK_PROCESSOR_NAME, "processorVersion": env!("CARGO_PKG_VERSION"), "sourceEventKey": observation.event_key.clone() } }), }; tracing::debug!(target: crate::TRACING_TARGET_MATERIALIZER_RISK, action = "materialize_token_risk_fact", risk_kind, operation, ata, "materialize committed SPL Token or ATA risk fact"); return crate::MtApiMaterializerExecutionResult { status: crate::MtApiMaterializerOutcomeStatus::Inserted, outputs: std::vec![output], diagnostics: std::vec::Vec::new(), }; } } fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> bool { let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID && event.surface_code.0 == "spl.token") || (event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID && event.surface_code.0 == "spl.token_2022"); let token_risk = token_program && RISK_ENTRIES.contains(&event.event_name.0.as_str()); return ACCEPTED_FAMILIES.contains(&event.event_family) && (token_risk || is_ata_recovery(event)); } fn is_ata_recovery(event: &crate::MdDecodedProtocolEvent) -> bool { return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID && event.surface_code.0 == "spl.associated_token_account" && event.event_family == crate::MdEventFamily::Lifecycle && event.event_name.0 == "recover_nested"; } fn ata_addresses_valid(payload: &serde_json::Value) -> bool { let addresses = match payload.get("addresses") { std::option::Option::Some(value) => value, std::option::Option::None => return false, }; let token_supported = addresses .get("tokenProgram") .and_then(|value| return value.get("supported")) .and_then(serde_json::Value::as_bool) == std::option::Option::Some(true); let token_program_id = addresses .get("tokenProgram") .and_then(|value| return value.get("programId")) .and_then(serde_json::Value::as_str); let token_program_exact = matches!( token_program_id, std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID) | std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID) ); return token_supported && token_program_exact && [ "ownerAssociatedTokenAccount", "nestedAssociatedTokenAccount", "walletNestedMintAssociatedTokenAccount", ] .iter() .all(|field| { let address = match addresses.get(*field) { std::option::Option::Some(value) => value, std::option::Option::None => return false, }; return address .get("observed") .and_then(serde_json::Value::as_str) .is_some_and(|value| return !value.is_empty()) && address.get("valid").and_then(serde_json::Value::as_bool) == std::option::Option::Some(true); }); } fn risk_kind( operation: &str, parameters: &serde_json::Value, accounts: &serde_json::Value, ) -> std::option::Option<&'static str> { return match operation { "recover_nested" => std::option::Option::Some("nested_ata_anti_pattern_recovered"), "approve" | "approve_checked" => std::option::Option::Some("delegation_allowance_granted"), "revoke" => std::option::Option::Some("delegation_revoked"), "freeze_account" => std::option::Option::Some("account_frozen"), "thaw_account" => std::option::Option::Some("account_thawed"), "set_authority" if parameters.get("newAuthority").is_some_and(serde_json::Value::is_null) => { std::option::Option::Some("authority_revoked") }, "set_authority" => std::option::Option::Some("authority_changed"), "initialize_default_account_state" | "update_default_account_state" => { std::option::Option::Some("default_account_state_configured") }, "enable_required_transfer_memos" => { std::option::Option::Some("required_transfer_memos_enabled") }, "disable_required_transfer_memos" => { std::option::Option::Some("required_transfer_memos_disabled") }, "enable_cpi_guard" => std::option::Option::Some("cpi_guard_enabled"), "disable_cpi_guard" => std::option::Option::Some("cpi_guard_disabled"), "initialize_non_transferable_mint" => { std::option::Option::Some("non_transferable_mint_configured") }, "initialize_pausable_config" => std::option::Option::Some("pausable_mint_configured"), "pause" => std::option::Option::Some("mint_activity_paused"), "resume" => std::option::Option::Some("mint_activity_resumed"), "initialize_permissioned_burn" => std::option::Option::Some("permissioned_burn_configured"), "enable_confidential_credits" => std::option::Option::Some("confidential_credits_enabled"), "disable_confidential_credits" => { std::option::Option::Some("confidential_credits_disabled") }, "enable_non_confidential_credits" => { std::option::Option::Some("non_confidential_credits_enabled") }, "disable_non_confidential_credits" => { std::option::Option::Some("non_confidential_credits_disabled") }, "initialize_multisig" | "initialize_multisig2" => { let m = parameters.get("m").and_then(serde_json::Value::as_u64).unwrap_or(0); let n = role_count(accounts, "multisig_member"); if m == 1 && n > 1 { std::option::Option::Some("weak_multisig_threshold") } else { std::option::Option::None } }, _ => std::option::Option::None, }; } fn role_count(accounts: &serde_json::Value, role: &str) -> usize { return accounts .as_array() .map(|rows| { return rows .iter() .filter(|row| { return row.get("role").and_then(serde_json::Value::as_str) == std::option::Option::Some(role); }) .count(); }) .unwrap_or(0); } #[cfg(test)] mod tests { fn observation( entry: &str, family: crate::MdEventFamily, parameters: serde_json::Value, failed: bool, ) -> crate::DcApiDecodedObservation { return crate::DcApiDecodedObservation { event_key: format!("token:{entry}:0"), event: crate::MdDecodedProtocolEvent { signature: crate::MdSignature("signature".to_string()), slot: crate::MdSlot(1), instruction_path: crate::MdInstructionPath("0".to_string()), program_id: crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), protocol_code: crate::MdProtocolCode("spl.token".to_string()), surface_code: crate::MdSurfaceCode("spl.token".to_string()), event_code: crate::MdEventCode(format!("spl.token.{entry}")), event_name: crate::MdEventName(entry.to_string()), event_family: family, source_kind: crate::MdEventSourceKind::Instruction, confidence: crate::MdDecoderConfidence::ManualExact, }, payload_json: serde_json::json!({"parameters":parameters,"accounts":[{"role":"multisig_member"},{"role":"multisig_member"}],"authority":{}}), transaction_failed: failed, transaction_error: std::option::Option::None, observation_committed: !failed, proof: crate::DcApiDecoderProof { kind: crate::DcApiDecoderProofKind::Manual, confidence: crate::MdDecoderConfidence::ManualExact, evidence: std::vec!["fixture".to_string()], }, }; } fn token_2022_observation( entry: &str, family: crate::MdEventFamily, parameters: serde_json::Value, failed: bool, ) -> crate::DcApiDecodedObservation { let mut observation = observation(entry, family, parameters, failed); observation.event.program_id = crate::MdProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()); observation.event.protocol_code = crate::MdProtocolCode("spl.token_2022".to_string()); observation.event.surface_code = crate::MdSurfaceCode("spl.token_2022".to_string()); observation.event.event_code = crate::MdEventCode(format!("spl.token_2022.{entry}")); observation.event_key = format!("token_2022:{entry}:0"); return observation; } fn nested_ata_observation(failed: bool, valid: bool) -> crate::DcApiDecodedObservation { return crate::DcApiDecodedObservation { event_key: "ata:recover_nested:0".to_string(), event: crate::MdDecodedProtocolEvent { signature: crate::MdSignature("signature".to_string()), slot: crate::MdSlot(2), instruction_path: crate::MdInstructionPath("1/0".to_string()), program_id: crate::MdProgramId( kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(), ), protocol_code: crate::MdProtocolCode("spl.associated_token_account".to_string()), surface_code: crate::MdSurfaceCode("spl.associated_token_account".to_string()), event_code: crate::MdEventCode( "spl.associated_token_account.recover_nested".to_string(), ), event_name: crate::MdEventName("recover_nested".to_string()), event_family: crate::MdEventFamily::Lifecycle, source_kind: crate::MdEventSourceKind::InnerInstruction, confidence: crate::MdDecoderConfidence::ManualExact, }, payload_json: serde_json::json!({ "addresses": { "tokenProgram": { "programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, "kind": "token_2022", "supported": true }, "ownerAssociatedTokenAccount": {"observed":"ownerAta","valid":valid}, "nestedAssociatedTokenAccount": {"observed":"nestedAta","valid":valid}, "walletNestedMintAssociatedTokenAccount": {"observed":"destinationAta","valid":valid} } }), transaction_failed: failed, transaction_error: std::option::Option::None, observation_committed: !failed, proof: crate::DcApiDecoderProof { kind: crate::DcApiDecoderProofKind::Manual, confidence: crate::MdDecoderConfidence::ManualExact, evidence: std::vec!["fixture".to_string()], }, }; } #[test] fn approval_and_authority_revocation_are_fact_only_without_score() { for (entry, family, parameters, expected) in [ ( "approve_checked", crate::MdEventFamily::TokenAccount, serde_json::json!({"amountRaw":"10"}), "delegation_allowance_granted", ), ( "set_authority", crate::MdEventFamily::Admin, serde_json::json!({"newAuthority":null}), "authority_revoked", ), ] { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &observation(entry, family, parameters, false), ); assert_eq!(result.outputs[0].payload_json["riskKind"], expected); assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null()); } } #[test] fn token_2022_revocation_is_owned_once_without_an_invented_score() { let observation = token_2022_observation( "set_authority", crate::MdEventFamily::Admin, serde_json::json!({"newAuthority":null}), false, ); let result = crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &observation); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs.len(), 1); assert_eq!(result.outputs[0].payload_json["riskKind"], "authority_revoked"); assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null()); } #[test] fn only_structurally_weak_multisig_creates_risk_fact() { let weak = observation( "initialize_multisig2", crate::MdEventFamily::Admin, serde_json::json!({"m":1}), false, ); let strong = observation( "initialize_multisig2", crate::MdEventFamily::Admin, serde_json::json!({"m":2}), false, ); assert_eq!( crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &weak).status, crate::MtApiMaterializerOutcomeStatus::Inserted ); assert_eq!( crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &strong).status, crate::MtApiMaterializerOutcomeStatus::Ignored ); } #[test] fn failed_risk_fact_is_refused() { let failed = observation( "freeze_account", crate::MdEventFamily::TokenAccount, serde_json::json!({}), true, ); assert_eq!( crate::MtApiEventMaterializer::materialize(&crate::MtRiskMaterializer, &failed).status, crate::MtApiMaterializerOutcomeStatus::Refused ); } #[test] fn committed_nested_ata_recovery_creates_one_fact_without_score() { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &nested_ata_observation(false, true), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs.len(), 1); assert_eq!( result.outputs[0].output_key, "ata_risk_fact:nested_ata_anti_pattern_recovered:0" ); assert_eq!(result.outputs[0].payload_json["riskKind"], "nested_ata_anti_pattern_recovered"); assert_eq!( result.outputs[0].payload_json["domain"], "spl_associated_token_account_risk_fact" ); assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null()); assert!(result.outputs[0].payload_json["interpretation"]["severity"].is_null()); } #[test] fn failed_or_invalid_nested_ata_recovery_creates_no_risk_fact() { for observation in [nested_ata_observation(true, true), nested_ata_observation(false, false)] { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &observation, ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); assert!(result.outputs.is_empty()); } } #[test] fn ata_matrix_declares_the_single_compiled_risk_fact() { let matrix: serde_json::Value = match serde_json::from_str(include_str!( "../../../../test-fixtures/contract-matrices/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json" )) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("ATA matrix is invalid: {error}"), }; let instructions = match matrix["instructions"].as_array() { std::option::Option::Some(value) => value, std::option::Option::None => panic!("ATA matrix instructions are absent"), }; let recover = match instructions.iter().find(|row| { return row.get("name").and_then(serde_json::Value::as_str) == std::option::Option::Some("recover_nested"); }) { std::option::Option::Some(value) => value, std::option::Option::None => panic!("RecoverNested matrix entry is absent"), }; assert!(recover["authorizedProjections"].as_array().is_some_and(|values| { return values.iter().any(|value| { return value == "risk:nested_ata_anti_pattern_recovered"; }); })); assert_eq!( matrix["materializationContract"]["projectionOwners"][1]["facts"], serde_json::json!(["nested_ata_anti_pattern_recovered"]) ); } #[test] fn token_2022_compliance_toggles_are_projected_as_fact_only_risk_rows() { for (entry, expected) in [ ("initialize_default_account_state", "default_account_state_configured"), ("enable_required_transfer_memos", "required_transfer_memos_enabled"), ("disable_required_transfer_memos", "required_transfer_memos_disabled"), ("enable_cpi_guard", "cpi_guard_enabled"), ("disable_cpi_guard", "cpi_guard_disabled"), ] { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &token_2022_observation( entry, crate::MdEventFamily::Audit, serde_json::json!({}), false, ), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs[0].payload_json["riskKind"], expected); assert_eq!(result.outputs[0].payload_json["interpretation"]["factOnly"], true); assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null()); } } #[test] fn token_2022_restriction_and_pause_facts_have_unique_stable_kinds() { for (entry, family, expected) in [ ( "initialize_non_transferable_mint", crate::MdEventFamily::TokenMint, "non_transferable_mint_configured", ), ( "initialize_pausable_config", crate::MdEventFamily::TokenAccount, "pausable_mint_configured", ), ("pause", crate::MdEventFamily::TokenAccount, "mint_activity_paused"), ("resume", crate::MdEventFamily::TokenAccount, "mint_activity_resumed"), ( "initialize_permissioned_burn", crate::MdEventFamily::TokenAccount, "permissioned_burn_configured", ), ] { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &token_2022_observation(entry, family, serde_json::json!({}), false), ); assert_eq!(result.outputs[0].payload_json["riskKind"], expected); } } #[test] fn confidential_credit_toggles_are_fact_only_and_keep_wire_parameters() { for (entry, credit_kind, enabled, expected) in [ ( "enable_confidential_credits", "confidential", true, "confidential_credits_enabled", ), ( "disable_confidential_credits", "confidential", false, "confidential_credits_disabled", ), ( "enable_non_confidential_credits", "non_confidential", true, "non_confidential_credits_enabled", ), ( "disable_non_confidential_credits", "non_confidential", false, "non_confidential_credits_disabled", ), ] { let result = crate::MtApiEventMaterializer::materialize( &crate::MtRiskMaterializer, &token_2022_observation( entry, crate::MdEventFamily::TokenAccount, serde_json::json!({ "extension":"confidential_transfer", "creditKind":credit_kind, "enabled":enabled, }), false, ), ); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs.len(), 1); assert_eq!(result.outputs[0].payload_json["riskKind"], expected); assert_eq!(result.outputs[0].payload_json["parameters"]["creditKind"], credit_kind); assert_eq!(result.outputs[0].payload_json["parameters"]["enabled"], enabled); assert_eq!(result.outputs[0].payload_json["interpretation"]["factOnly"], true); assert!(result.outputs[0].payload_json["interpretation"]["score"].is_null()); } } }