|
|
|
|
@@ -0,0 +1,646 @@
|
|
|
|
|
// file: kb_materializer_risk/src/materializer.rs
|
|
|
|
|
// version: 10
|
|
|
|
|
|
|
|
|
|
//! Stable fact-only risk projections for committed SPL Token and ATA observations.
|
|
|
|
|
|
|
|
|
|
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
kb_model::EventFamily::TokenMint,
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
kb_model::EventFamily::Lifecycle,
|
|
|
|
|
kb_model::EventFamily::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 RiskMaterializer;
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::Materializer for crate::RiskMaterializer {
|
|
|
|
|
fn materializer_name(&self) -> &'static str {
|
|
|
|
|
return "kb_materializer_risk";
|
|
|
|
|
}
|
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
|
|
|
return env!("CARGO_PKG_VERSION");
|
|
|
|
|
}
|
|
|
|
|
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return accepts_event(event);
|
|
|
|
|
}
|
|
|
|
|
fn materialize_event(
|
|
|
|
|
&self,
|
|
|
|
|
_event: &kb_model::DecodedProtocolEvent,
|
|
|
|
|
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
|
|
|
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl kb_materializer_api::EventMaterializer for crate::RiskMaterializer {
|
|
|
|
|
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
return kb_materializer_api::MaterializerIdentity {
|
|
|
|
|
name: crate::PROCESSOR_NAME.to_string(),
|
|
|
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
|
|
|
|
|
return ACCEPTED_FAMILIES;
|
|
|
|
|
}
|
|
|
|
|
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
|
|
|
|
|
return accepts_event(&observation.event);
|
|
|
|
|
}
|
|
|
|
|
fn transaction_policy(
|
|
|
|
|
&self,
|
|
|
|
|
_family: kb_model::EventFamily,
|
|
|
|
|
) -> kb_materializer_api::MaterializationTransactionPolicy {
|
|
|
|
|
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
|
|
|
}
|
|
|
|
|
fn materialize(
|
|
|
|
|
&self,
|
|
|
|
|
observation: &kb_decoder_api::DecodedObservation,
|
|
|
|
|
) -> kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
if !accepts_event(&observation.event) {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::ignored();
|
|
|
|
|
}
|
|
|
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult::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 kb_materializer_api::MaterializerExecutionResult::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 kb_materializer_api::MaterializerExecutionResult::ignored();
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let output = kb_materializer_api::MaterializedOutput {
|
|
|
|
|
output_key: if ata {
|
|
|
|
|
format!("ata_risk_fact:{risk_kind}:0")
|
|
|
|
|
} else {
|
|
|
|
|
format!("spl_token_risk_fact:{risk_kind}:0")
|
|
|
|
|
},
|
|
|
|
|
family: kb_model::MaterializedEventFamily::Risk,
|
|
|
|
|
payload_json: serde_json::json!({
|
|
|
|
|
"projectionVersion": crate::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::PROCESSOR_NAME,
|
|
|
|
|
"processorVersion": env!("CARGO_PKG_VERSION"),
|
|
|
|
|
"sourceEventKey": observation.event_key.clone()
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
tracing::debug!(target: crate::TRACING_TARGET, action = "materialize_token_risk_fact", risk_kind, operation, ata, "materialize committed SPL Token or ATA risk fact");
|
|
|
|
|
return kb_materializer_api::MaterializerExecutionResult {
|
|
|
|
|
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
|
|
|
|
|
outputs: std::vec![output],
|
|
|
|
|
diagnostics: std::vec::Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn accepts_event(event: &kb_model::DecodedProtocolEvent) -> 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: &kb_model::DecodedProtocolEvent) -> bool {
|
|
|
|
|
return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
|
|
|
|
|
&& event.surface_code.0 == "spl_associated_token_account"
|
|
|
|
|
&& event.event_family == kb_model::EventFamily::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: kb_model::EventFamily,
|
|
|
|
|
parameters: serde_json::Value,
|
|
|
|
|
failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return kb_decoder_api::DecodedObservation {
|
|
|
|
|
event_key: format!("token:{entry}:0"),
|
|
|
|
|
event: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(1),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("spl_token".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode("spl_token".to_string()),
|
|
|
|
|
event_code: kb_model::EventCode(format!("spl_token.{entry}")),
|
|
|
|
|
event_name: kb_model::EventName(entry.to_string()),
|
|
|
|
|
event_family: family,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::Instruction,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::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: kb_decoder_api::DecoderProof {
|
|
|
|
|
kind: kb_decoder_api::DecoderProofKind::Manual,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::ManualExact,
|
|
|
|
|
evidence: std::vec!["fixture".to_string()],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn token_2022_observation(
|
|
|
|
|
entry: &str,
|
|
|
|
|
family: kb_model::EventFamily,
|
|
|
|
|
parameters: serde_json::Value,
|
|
|
|
|
failed: bool,
|
|
|
|
|
) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
let mut observation = observation(entry, family, parameters, failed);
|
|
|
|
|
observation.event.program_id =
|
|
|
|
|
kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
|
|
|
|
|
observation.event.protocol_code = kb_model::ProtocolCode("spl_token_2022".to_string());
|
|
|
|
|
observation.event.surface_code = kb_model::SurfaceCode("spl_token_2022".to_string());
|
|
|
|
|
observation.event.event_code = kb_model::EventCode(format!("spl_token_2022.{entry}"));
|
|
|
|
|
observation.event_key = format!("token_2022:{entry}:0");
|
|
|
|
|
return observation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn nested_ata_observation(failed: bool, valid: bool) -> kb_decoder_api::DecodedObservation {
|
|
|
|
|
return kb_decoder_api::DecodedObservation {
|
|
|
|
|
event_key: "ata:recover_nested:0".to_string(),
|
|
|
|
|
event: kb_model::DecodedProtocolEvent {
|
|
|
|
|
signature: kb_model::Signature("signature".to_string()),
|
|
|
|
|
slot: kb_model::Slot(2),
|
|
|
|
|
instruction_path: kb_model::InstructionPath("1/0".to_string()),
|
|
|
|
|
program_id: kb_model::ProgramId(
|
|
|
|
|
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
|
|
|
|
),
|
|
|
|
|
protocol_code: kb_model::ProtocolCode("spl_associated_token_account".to_string()),
|
|
|
|
|
surface_code: kb_model::SurfaceCode("spl_associated_token_account".to_string()),
|
|
|
|
|
event_code: kb_model::EventCode(
|
|
|
|
|
"spl_associated_token_account.recover_nested".to_string(),
|
|
|
|
|
),
|
|
|
|
|
event_name: kb_model::EventName("recover_nested".to_string()),
|
|
|
|
|
event_family: kb_model::EventFamily::Lifecycle,
|
|
|
|
|
source_kind: kb_model::EventSourceKind::InnerInstruction,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::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: kb_decoder_api::DecoderProof {
|
|
|
|
|
kind: kb_decoder_api::DecoderProofKind::Manual,
|
|
|
|
|
confidence: kb_model::DecoderConfidence::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",
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
serde_json::json!({"amountRaw":"10"}),
|
|
|
|
|
"delegation_allowance_granted",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"set_authority",
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
serde_json::json!({"newAuthority":null}),
|
|
|
|
|
"authority_revoked",
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&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",
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
serde_json::json!({"newAuthority":null}),
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::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",
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
serde_json::json!({"m":1}),
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
let strong = observation(
|
|
|
|
|
"initialize_multisig2",
|
|
|
|
|
kb_model::EventFamily::Admin,
|
|
|
|
|
serde_json::json!({"m":2}),
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&crate::RiskMaterializer, &weak)
|
|
|
|
|
.status,
|
|
|
|
|
kb_materializer_api::MaterializerOutcomeStatus::Inserted
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&crate::RiskMaterializer, &strong)
|
|
|
|
|
.status,
|
|
|
|
|
kb_materializer_api::MaterializerOutcomeStatus::Ignored
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_risk_fact_is_refused() {
|
|
|
|
|
let failed = observation(
|
|
|
|
|
"freeze_account",
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
serde_json::json!({}),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
kb_materializer_api::EventMaterializer::materialize(&crate::RiskMaterializer, &failed)
|
|
|
|
|
.status,
|
|
|
|
|
kb_materializer_api::MaterializerOutcomeStatus::Refused
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn committed_nested_ata_recovery_creates_one_fact_without_score() {
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&nested_ata_observation(false, true),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::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 = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&observation,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::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!(
|
|
|
|
|
"../../docs/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 = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&token_2022_observation(
|
|
|
|
|
entry,
|
|
|
|
|
kb_model::EventFamily::Audit,
|
|
|
|
|
serde_json::json!({}),
|
|
|
|
|
false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::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",
|
|
|
|
|
kb_model::EventFamily::TokenMint,
|
|
|
|
|
"non_transferable_mint_configured",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"initialize_pausable_config",
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
"pausable_mint_configured",
|
|
|
|
|
),
|
|
|
|
|
("pause", kb_model::EventFamily::TokenAccount, "mint_activity_paused"),
|
|
|
|
|
("resume", kb_model::EventFamily::TokenAccount, "mint_activity_resumed"),
|
|
|
|
|
(
|
|
|
|
|
"initialize_permissioned_burn",
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
"permissioned_burn_configured",
|
|
|
|
|
),
|
|
|
|
|
] {
|
|
|
|
|
let result = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&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 = kb_materializer_api::EventMaterializer::materialize(
|
|
|
|
|
&crate::RiskMaterializer,
|
|
|
|
|
&token_2022_observation(
|
|
|
|
|
entry,
|
|
|
|
|
kb_model::EventFamily::TokenAccount,
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"extension":"confidential_transfer",
|
|
|
|
|
"creditKind":credit_kind,
|
|
|
|
|
"enabled":enabled,
|
|
|
|
|
}),
|
|
|
|
|
false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|