From c735090dddc1e39e406b84e83826f851af3731f6 Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Mon, 22 Jun 2026 12:11:21 +0530 Subject: [PATCH 1/5] feat(contracts): add credential security auditing and activity tracking --- contracts/src/auditing.rs | 114 ++++++++++++++++++++++++++++++++++++++ contracts/src/errors.rs | 5 ++ contracts/src/events.rs | 9 +++ contracts/src/lib.rs | 7 +++ contracts/src/storage.rs | 7 +++ contracts/src/test.rs | 104 ++++++++++++++++++++++++++++++++++ contracts/src/types.rs | 21 +++++++ 7 files changed, 267 insertions(+) create mode 100644 contracts/src/auditing.rs create mode 100644 contracts/src/events.rs create mode 100644 contracts/src/storage.rs create mode 100644 contracts/src/test.rs create mode 100644 contracts/src/types.rs diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs new file mode 100644 index 00000000..1ebce5cd --- /dev/null +++ b/contracts/src/auditing.rs @@ -0,0 +1,114 @@ +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Symbol, Vec}; + +use crate::errors::Error; +use crate::events::emit_audit_event; +use crate::storage::AuditDataKey; +use crate::types::AuditRecord; + +#[contract] +pub struct Auditing; + +#[contractimpl] +impl Auditing { + pub fn get_audit_report(env: Env, credential_id: BytesN<32>) -> Vec { + env.storage() + .persistent() + .get(&AuditDataKey::AuditRecords(credential_id)) + .unwrap_or(Vec::new(&env)) + } + + pub fn log_audit_event( + env: Env, + credential_id: BytesN<32>, + actor: Address, + action: Symbol, + details: String, + ) { + let timestamp = env.ledger().timestamp(); + let record = AuditRecord { + credential_id: credential_id.clone(), + actor: actor.clone(), + action: action.clone(), + timestamp, + details, + }; + + let mut records = Self::get_audit_report(env.clone(), credential_id.clone()); + records.push_back(record.clone()); + + env.storage() + .persistent() + .set(&AuditDataKey::AuditRecords(credential_id.clone()), &records); + + emit_audit_event(&env, actor, record); + } + + pub fn track_activity(env: Env, actor: Address, is_failure: bool) -> Result<(), Error> { + let mut failed_attempts: u32 = env + .storage() + .instance() + .get(&AuditDataKey::ActivityTracker(actor.clone())) + .unwrap_or(0); + + if is_failure { + failed_attempts += 1; + } else { + failed_attempts = 0; + } + + env.storage() + .instance() + .set(&AuditDataKey::ActivityTracker(actor.clone()), &failed_attempts); + + if failed_attempts > 5 { + Self::log_audit_event( + env.clone(), + BytesN::from_array(&env, &[0; 32]), // Dummy ID for suspicious actor without specific credential + actor.clone(), + Symbol::new(&env, "suspicious"), + String::from_str(&env, "Excessive verification failures"), + ); + return Err(Error::SuspiciousActivity); + } + + Ok(()) + } + + pub fn require_auth(actor: &Address) { + actor.require_auth(); + } + + pub fn credential_exists(env: &Env, credential_id: &BytesN<32>) -> Result<(), Error> { + let records: Option> = env + .storage() + .persistent() + .get(&AuditDataKey::AuditRecords(credential_id.clone())); + if records.is_none() { + return Err(Error::CredentialNotFound); + } + Ok(()) + } + + pub fn credential_not_revoked(env: &Env, credential_id: &BytesN<32>) -> Result<(), Error> { + let records = Self::get_audit_report(env.clone(), credential_id.clone()); + for record in records.iter() { + if record.action == Symbol::new(env, "revoked") { + return Err(Error::CredentialRevoked); + } + } + Ok(()) + } + + pub fn issuer_authorized(env: &Env, issuer: &Address, credential_id: &BytesN<32>) -> Result<(), Error> { + let records = Self::get_audit_report(env.clone(), credential_id.clone()); + for record in records.iter() { + if record.action == Symbol::new(env, "issued") { + if record.actor != *issuer { + return Err(Error::IssuerNotAuthorized); + } + return Ok(()); + } + } + Ok(()) + } +} diff --git a/contracts/src/errors.rs b/contracts/src/errors.rs index d90594e7..09be593e 100644 --- a/contracts/src/errors.rs +++ b/contracts/src/errors.rs @@ -21,4 +21,9 @@ pub enum Error { MigrationFailed = 14, VersionMismatch = 15, StorageCorrupted = 16, + // Auditing errors + SuspiciousActivity = 17, + IssuerNotAuthorized = 18, + CredentialRevoked = 19, + CredentialNotFound = 20, } diff --git a/contracts/src/events.rs b/contracts/src/events.rs new file mode 100644 index 00000000..2bb2a25e --- /dev/null +++ b/contracts/src/events.rs @@ -0,0 +1,9 @@ +use soroban_sdk::{symbol_short, Address, Env}; +use crate::types::AuditRecord; + +pub fn emit_audit_event(env: &Env, actor: Address, audit_record: AuditRecord) { + env.events().publish( + (symbol_short!("audit"), actor), + audit_record, + ); +} diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 8326ce1f..dd19208c 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -6,6 +6,10 @@ pub mod errors; pub mod identity_registry; pub mod upgrade; pub mod verification; +pub mod types; +pub mod events; +pub mod storage; +pub mod auditing; #[cfg(test)] mod upgrade_tests; @@ -16,6 +20,9 @@ mod integration_tests; #[cfg(test)] mod benchmarks; +#[cfg(test)] +mod test; + pub use access_control::AccessControl; pub use data_sharing::DataSharing; pub use errors::Error; diff --git a/contracts/src/storage.rs b/contracts/src/storage.rs new file mode 100644 index 00000000..252bc0c1 --- /dev/null +++ b/contracts/src/storage.rs @@ -0,0 +1,7 @@ +use soroban_sdk::{contracttype, Address, BytesN}; + +#[contracttype] +pub enum AuditDataKey { + AuditRecords(BytesN<32>), + ActivityTracker(Address), +} diff --git a/contracts/src/test.rs b/contracts/src/test.rs new file mode 100644 index 00000000..93d6ff44 --- /dev/null +++ b/contracts/src/test.rs @@ -0,0 +1,104 @@ +#![cfg(test)] + +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol}; +use crate::auditing::{Auditing, AuditingClient}; +use crate::errors::Error; + +#[test] +fn test_audit_record_created() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[1; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "Credential created"), + ); + + let report = client.get_audit_report(&credential_id); + assert_eq!(report.len(), 1); + let record = report.get(0).unwrap(); + assert_eq!(record.credential_id, credential_id); + assert_eq!(record.actor, actor); + assert_eq!(record.action, Symbol::new(&env, "issued")); +} + +#[test] +fn test_suspicious_activity_logged() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + + for _ in 0..5 { + client.track_activity(&actor, &true); + } + + // 6th failure should trigger suspicious activity + let res = client.try_track_activity(&actor, &true); + assert!(res.is_err()); + + // Verify dummy credential ID was used for the event + let dummy_id = BytesN::from_array(&env, &[0; 32]); + let report = client.get_audit_report(&dummy_id); + assert_eq!(report.len(), 1); + let record = report.get(0).unwrap(); + assert_eq!(record.action, Symbol::new(&env, "suspicious")); +} + +#[test] +fn test_security_check_failure() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let credential_id = BytesN::from_array(&env, &[2; 32]); + + let res = client.try_credential_exists(&credential_id); + assert!(res.is_err()); +} + +#[test] +fn test_audit_report_generation() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[3; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "first"), + ); + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "verified"), + &String::from_str(&env, "second"), + ); + + let report = client.get_audit_report(&credential_id); + assert_eq!(report.len(), 2); +} + +#[test] +fn test_events_emitted() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[4; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "Emit test"), + ); + assert!(true); +} diff --git a/contracts/src/types.rs b/contracts/src/types.rs new file mode 100644 index 00000000..d60784d8 --- /dev/null +++ b/contracts/src/types.rs @@ -0,0 +1,21 @@ +use soroban_sdk::{contracttype, Address, BytesN, String, Symbol}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AuditEventType { + CredentialIssued, + CredentialUpdated, + CredentialRevoked, + CredentialVerified, + SuspiciousActivityDetected, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuditRecord { + pub credential_id: BytesN<32>, + pub actor: Address, + pub action: Symbol, + pub timestamp: u64, + pub details: String, +} From 52dcee93161963140eb31607b8b3d3175e8ef628 Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Mon, 22 Jun 2026 13:17:07 +0530 Subject: [PATCH 2/5] style(contracts): fix formatting issues in credential security audits --- contracts/src/auditing.rs | 13 +++++++++---- contracts/src/events.rs | 8 +++----- contracts/src/lib.rs | 8 ++++---- contracts/src/test.rs | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs index 1ebce5cd..dcd81c56 100644 --- a/contracts/src/auditing.rs +++ b/contracts/src/auditing.rs @@ -56,9 +56,10 @@ impl Auditing { failed_attempts = 0; } - env.storage() - .instance() - .set(&AuditDataKey::ActivityTracker(actor.clone()), &failed_attempts); + env.storage().instance().set( + &AuditDataKey::ActivityTracker(actor.clone()), + &failed_attempts, + ); if failed_attempts > 5 { Self::log_audit_event( @@ -99,7 +100,11 @@ impl Auditing { Ok(()) } - pub fn issuer_authorized(env: &Env, issuer: &Address, credential_id: &BytesN<32>) -> Result<(), Error> { + pub fn issuer_authorized( + env: &Env, + issuer: &Address, + credential_id: &BytesN<32>, + ) -> Result<(), Error> { let records = Self::get_audit_report(env.clone(), credential_id.clone()); for record in records.iter() { if record.action == Symbol::new(env, "issued") { diff --git a/contracts/src/events.rs b/contracts/src/events.rs index 2bb2a25e..826848b4 100644 --- a/contracts/src/events.rs +++ b/contracts/src/events.rs @@ -1,9 +1,7 @@ -use soroban_sdk::{symbol_short, Address, Env}; use crate::types::AuditRecord; +use soroban_sdk::{symbol_short, Address, Env}; pub fn emit_audit_event(env: &Env, actor: Address, audit_record: AuditRecord) { - env.events().publish( - (symbol_short!("audit"), actor), - audit_record, - ); + env.events() + .publish((symbol_short!("audit"), actor), audit_record); } diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index dd19208c..b3dc34c0 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -1,15 +1,15 @@ #![no_std] pub mod access_control; +pub mod auditing; pub mod data_sharing; pub mod errors; +pub mod events; pub mod identity_registry; +pub mod storage; +pub mod types; pub mod upgrade; pub mod verification; -pub mod types; -pub mod events; -pub mod storage; -pub mod auditing; #[cfg(test)] mod upgrade_tests; diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 93d6ff44..ab0795cc 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -1,8 +1,8 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol}; use crate::auditing::{Auditing, AuditingClient}; use crate::errors::Error; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol}; #[test] fn test_audit_record_created() { From 2009e5fa2616e6fa816e4b4bb190bb5a0c583b2b Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Mon, 22 Jun 2026 13:22:21 +0530 Subject: [PATCH 3/5] fix(contracts): fix method signatures for AuditingClient generation --- contracts/src/auditing.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs index dcd81c56..1fb54e80 100644 --- a/contracts/src/auditing.rs +++ b/contracts/src/auditing.rs @@ -75,25 +75,25 @@ impl Auditing { Ok(()) } - pub fn require_auth(actor: &Address) { + pub fn require_auth(env: Env, actor: Address) { actor.require_auth(); } - pub fn credential_exists(env: &Env, credential_id: &BytesN<32>) -> Result<(), Error> { + pub fn credential_exists(env: Env, credential_id: BytesN<32>) -> Result<(), Error> { let records: Option> = env .storage() .persistent() - .get(&AuditDataKey::AuditRecords(credential_id.clone())); + .get(&AuditDataKey::AuditRecords(credential_id)); if records.is_none() { return Err(Error::CredentialNotFound); } Ok(()) } - pub fn credential_not_revoked(env: &Env, credential_id: &BytesN<32>) -> Result<(), Error> { - let records = Self::get_audit_report(env.clone(), credential_id.clone()); + pub fn credential_not_revoked(env: Env, credential_id: BytesN<32>) -> Result<(), Error> { + let records = Self::get_audit_report(env.clone(), credential_id); for record in records.iter() { - if record.action == Symbol::new(env, "revoked") { + if record.action == Symbol::new(&env, "revoked") { return Err(Error::CredentialRevoked); } } @@ -101,14 +101,14 @@ impl Auditing { } pub fn issuer_authorized( - env: &Env, - issuer: &Address, - credential_id: &BytesN<32>, + env: Env, + issuer: Address, + credential_id: BytesN<32>, ) -> Result<(), Error> { - let records = Self::get_audit_report(env.clone(), credential_id.clone()); + let records = Self::get_audit_report(env.clone(), credential_id); for record in records.iter() { - if record.action == Symbol::new(env, "issued") { - if record.actor != *issuer { + if record.action == Symbol::new(&env, "issued") { + if record.actor != issuer { return Err(Error::IssuerNotAuthorized); } return Ok(()); From ef9e3d4ccd9a57f9f070480654538b98bed4f581 Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Mon, 22 Jun 2026 13:27:19 +0530 Subject: [PATCH 4/5] fix(contracts): fix clippy warnings in auditing tests and methods --- contracts/src/auditing.rs | 2 +- contracts/src/test.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs index 1fb54e80..26660e87 100644 --- a/contracts/src/auditing.rs +++ b/contracts/src/auditing.rs @@ -75,7 +75,7 @@ impl Auditing { Ok(()) } - pub fn require_auth(env: Env, actor: Address) { + pub fn require_auth(_env: Env, actor: Address) { actor.require_auth(); } diff --git a/contracts/src/test.rs b/contracts/src/test.rs index ab0795cc..262e47e7 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -1,7 +1,6 @@ #![cfg(test)] use crate::auditing::{Auditing, AuditingClient}; -use crate::errors::Error; use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol}; #[test] @@ -100,5 +99,4 @@ fn test_events_emitted() { &Symbol::new(&env, "issued"), &String::from_str(&env, "Emit test"), ); - assert!(true); } From a58348bc7e6ac1e89114f3a0aaa6c812e1ef6e64 Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Mon, 22 Jun 2026 13:30:49 +0530 Subject: [PATCH 5/5] fix(contracts): prevent storage rollback on suspicious activity tracking --- contracts/src/auditing.rs | 1 - contracts/src/test.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs index 26660e87..6adaa4e5 100644 --- a/contracts/src/auditing.rs +++ b/contracts/src/auditing.rs @@ -69,7 +69,6 @@ impl Auditing { Symbol::new(&env, "suspicious"), String::from_str(&env, "Excessive verification failures"), ); - return Err(Error::SuspiciousActivity); } Ok(()) diff --git a/contracts/src/test.rs b/contracts/src/test.rs index 262e47e7..171344f4 100644 --- a/contracts/src/test.rs +++ b/contracts/src/test.rs @@ -38,8 +38,7 @@ fn test_suspicious_activity_logged() { } // 6th failure should trigger suspicious activity - let res = client.try_track_activity(&actor, &true); - assert!(res.is_err()); + client.track_activity(&actor, &true); // Verify dummy credential ID was used for the event let dummy_id = BytesN::from_array(&env, &[0; 32]);