diff --git a/Cargo.lock b/Cargo.lock index cf26ee7..a38119a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.29" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "shlex", ] @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b7c5dbd637569a2cca66e8d66b8c446a1e7bf064ea321d265d7b3dfe7c97e" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", @@ -678,7 +678,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek 4.2.0", + "curve25519-dalek 4.1.3", "ed25519 2.2.3", "rand_core 0.6.4", "serde", @@ -769,9 +769,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.3.0" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "fnv" @@ -1661,9 +1661,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "indexmap 2.10.0", "itoa", @@ -1845,7 +1845,7 @@ dependencies = [ "ark-ec", "ark-ff", "ark-serialize", - "curve25519-dalek 4.2.0", + "curve25519-dalek 4.1.3", "ecdsa", "ed25519-dalek 2.2.0", "elliptic-curve", diff --git a/contracts/smart-account/src/account.rs b/contracts/smart-account/src/account.rs index 40f511e..92b8484 100644 --- a/contracts/smart-account/src/account.rs +++ b/contracts/smart-account/src/account.rs @@ -37,6 +37,42 @@ pub struct SignerRevokedEvent { pub revoked_signer: Signer, } +#[contracttype] +#[derive(Clone)] +pub struct AuthCheckFailedEvent { + pub error_code: u32, + pub error_message: soroban_sdk::String, + pub signer_key: Option, + pub context: Option, +} + +#[contracttype] +#[derive(Clone)] +pub struct SignerOperationFailedEvent { + pub operation: soroban_sdk::String, + pub error_code: u32, + pub error_message: soroban_sdk::String, + pub signer_key: Option, +} + +#[contracttype] +#[derive(Clone)] +pub struct PolicyValidationFailedEvent { + pub policy_type: soroban_sdk::String, + pub error_code: u32, + pub error_message: soroban_sdk::String, + pub signer_key: Option, +} + +#[contracttype] +#[derive(Clone)] +pub struct SignatureVerificationFailedEvent { + pub error_code: u32, + pub error_message: soroban_sdk::String, + pub signer_key: soroban_sdk::String, + pub proof_type: soroban_sdk::String, +} + /// SmartAccount is a multi-signature account contract that provides enhanced security /// through role-based access control and policy-based authorization. /// @@ -67,6 +103,31 @@ impl SmartAccount { env.current_contract_address().require_auth(); } } + + fn error_to_code_and_message(env: &Env, error: &Error) -> (u32, soroban_sdk::String) { + let (code, message) = match error { + Error::SignerNotFound => (1, "SignerNotFound"), + Error::SignerAlreadyExists => (2, "SignerAlreadyExists"), + Error::NoProofsInAuthEntry => (3, "NoProofsInAuthEntry"), + Error::InsufficientPermissions => (4, "InsufficientPermissions"), + Error::InsufficientPermissionsOnCreation => (5, "InsufficientPermissionsOnCreation"), + Error::CannotRevokeAdminSigner => (6, "CannotRevokeAdminSigner"), + Error::InvalidProofType => (7, "InvalidProofType"), + Error::SignatureVerificationFailed => (8, "SignatureVerificationFailed"), + Error::InvalidPolicy => (9, "InvalidPolicy"), + Error::InvalidNotAfterTime => (10, "InvalidNotAfterTime"), + Error::InvalidTimeRange => (11, "InvalidTimeRange"), + _ => (999, "UnknownError"), + }; + (code, soroban_sdk::String::from_str(env, message)) + } + + fn signer_key_to_string(env: &Env, signer_key: &SignerKey) -> soroban_sdk::String { + match signer_key { + SignerKey::Ed25519(_key) => soroban_sdk::String::from_str(env, "ed25519_key"), + SignerKey::Secp256r1(_key_id) => soroban_sdk::String::from_str(env, "secp256r1_key"), + } + } } // ============================================================================ @@ -91,6 +152,17 @@ impl SmartAccountInterface for SmartAccount { // Check that there is at least one admin signer to prevent the contract from being locked out. if !signers.iter().any(|s| s.role() == SignerRole::Admin) { + let (error_code, error_message) = + Self::error_to_code_and_message(&env, &Error::InsufficientPermissionsOnCreation); + env.events().publish( + (symbol_short!("constr"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(&env, "constructor"), + error_code, + error_message, + signer_key: None, + }, + ); panic_with_error!(env, Error::InsufficientPermissionsOnCreation); } @@ -98,6 +170,17 @@ impl SmartAccountInterface for SmartAccount { for signer in signers.iter() { let signer_key: SignerKey = signer.clone().into(); if seen_signer_keys.contains(&signer_key) { + let (error_code, error_message) = + Self::error_to_code_and_message(&env, &Error::SignerAlreadyExists); + env.events().publish( + (symbol_short!("constr"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(&env, "constructor"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(&env, &signer_key)), + }, + ); panic_with_error!(env, Error::SignerAlreadyExists); } seen_signer_keys.push_back(signer_key); @@ -107,9 +190,23 @@ impl SmartAccountInterface for SmartAccount { // If it's a restricted signer, we check that the policies are valid. if let SignerRole::Restricted(policies) = signer.role() { for policy in policies { - policy - .check(&env) - .unwrap_or_else(|e| panic_with_error!(env, e)); + if let Err(e) = policy.check(&env) { + let (error_code, error_message) = Self::error_to_code_and_message(&env, &e); + let signer_key: SignerKey = signer.clone().into(); + env.events().publish( + (symbol_short!("constr"), symbol_short!("failed")), + PolicyValidationFailedEvent { + policy_type: soroban_sdk::String::from_str( + &env, + "restricted_policy", + ), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(&env, &signer_key)), + }, + ); + panic_with_error!(env, e); + } } } SmartAccount::add_signer(&env, signer).unwrap_or_else(|e| panic_with_error!(env, e)); @@ -121,7 +218,23 @@ impl SmartAccountInterface for SmartAccount { fn add_signer(env: &Env, signer: Signer) -> Result<(), Error> { Self::require_auth_if_initialized(env); let key = signer.clone().into(); - Storage::default().store::(env, &key, &signer)?; + match Storage::default().store::(env, &key, &signer) { + Ok(_) => {} + Err(e) => { + let error: Error = e.into(); + let (error_code, error_message) = Self::error_to_code_and_message(env, &error); + env.events().publish( + (symbol_short!("signer"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(env, "add_signer"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(env, &key)), + }, + ); + return Err(error); + } + } let event = SignerAddedEvent { signer_key: key.clone(), @@ -138,7 +251,23 @@ impl SmartAccountInterface for SmartAccount { let key = signer.clone().into(); let storage = Storage::default(); - storage.update::(env, &key, &signer)?; + match storage.update::(env, &key, &signer) { + Ok(_) => {} + Err(e) => { + let error: Error = e.into(); + let (error_code, error_message) = Self::error_to_code_and_message(env, &error); + env.events().publish( + (symbol_short!("signer"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(env, "update_signer"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(env, &key)), + }, + ); + return Err(error); + } + } let event = SignerUpdatedEvent { signer_key: key.clone(), @@ -155,15 +284,56 @@ impl SmartAccountInterface for SmartAccount { let storage = Storage::default(); - let signer_to_revoke = storage - .get::(env, &signer_key) - .ok_or(Error::SignerNotFound)?; + let signer_to_revoke = match storage.get::(env, &signer_key) { + Some(signer) => signer, + None => { + let (error_code, error_message) = + Self::error_to_code_and_message(env, &Error::SignerNotFound); + env.events().publish( + (symbol_short!("signer"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(env, "revoke_signer"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(env, &signer_key)), + }, + ); + return Err(Error::SignerNotFound); + } + }; if signer_to_revoke.role() == SignerRole::Admin { + let (error_code, error_message) = + Self::error_to_code_and_message(env, &Error::CannotRevokeAdminSigner); + env.events().publish( + (symbol_short!("signer"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(env, "revoke_signer"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(env, &signer_key)), + }, + ); return Err(Error::CannotRevokeAdminSigner); } - storage.delete::(env, &signer_key)?; + match storage.delete::(env, &signer_key) { + Ok(_) => {} + Err(e) => { + let error: Error = e.into(); + let (error_code, error_message) = Self::error_to_code_and_message(env, &error); + env.events().publish( + (symbol_short!("signer"), symbol_short!("failed")), + SignerOperationFailedEvent { + operation: soroban_sdk::String::from_str(env, "revoke_signer"), + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(env, &signer_key)), + }, + ); + return Err(error); + } + } let event = SignerRevokedEvent { signer_key: signer_key.clone(), @@ -219,6 +389,17 @@ impl CustomAccountInterface for SmartAccount { // Ensure we have at least one authorization proof if proof_map.is_empty() { + let (error_code, error_message) = + Self::error_to_code_and_message(&env, &Error::NoProofsInAuthEntry); + env.events().publish( + (symbol_short!("auth"), symbol_short!("failed")), + AuthCheckFailedEvent { + error_code, + error_message, + signer_key: None, + context: Some(soroban_sdk::String::from_str(&env, "no_proofs_provided")), + }, + ); return Err(Error::NoProofsInAuthEntry); } @@ -228,6 +409,17 @@ impl CustomAccountInterface for SmartAccount { for (signer_key, _) in proof_map.iter() { if !storage.has(&env, &signer_key) { log!(&env, "Signer not found {:?}", signer_key); + let (error_code, error_message) = + Self::error_to_code_and_message(&env, &Error::SignerNotFound); + env.events().publish( + (symbol_short!("auth"), symbol_short!("failed")), + AuthCheckFailedEvent { + error_code, + error_message, + signer_key: Some(Self::signer_key_to_string(&env, &signer_key)), + context: Some(soroban_sdk::String::from_str(&env, "signer_lookup_failed")), + }, + ); return Err(Error::SignerNotFound); } } @@ -250,6 +442,20 @@ impl CustomAccountInterface for SmartAccount { } } if !context_authorized { + let (error_code, error_message) = + Self::error_to_code_and_message(&env, &Error::InsufficientPermissions); + env.events().publish( + (symbol_short!("auth"), symbol_short!("failed")), + AuthCheckFailedEvent { + error_code, + error_message, + signer_key: None, + context: Some(soroban_sdk::String::from_str( + &env, + "context_authorization_failed", + )), + }, + ); return Err(Error::InsufficientPermissions); } } diff --git a/contracts/smart-account/src/auth/policy/allow_list.rs b/contracts/smart-account/src/auth/policy/allow_list.rs index 127faee..23d9d3f 100644 --- a/contracts/smart-account/src/auth/policy/allow_list.rs +++ b/contracts/smart-account/src/auth/policy/allow_list.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{auth::Context, contracttype, Address, Env, Vec}; +use soroban_sdk::{auth::Context, contracttype, symbol_short, Address, Env, Vec}; use crate::{ auth::permissions::{AuthorizationCheck, PolicyValidator}, @@ -28,6 +28,15 @@ impl PolicyValidator for ContractAllowListPolicy { .allowed_contracts .contains(env.current_contract_address()) { + env.events().publish( + (symbol_short!("policy"), symbol_short!("failed")), + crate::account::PolicyValidationFailedEvent { + policy_type: soroban_sdk::String::from_str(env, "contract_allow_list"), + error_code: 9, + error_message: soroban_sdk::String::from_str(env, "InvalidPolicy"), + signer_key: None, + }, + ); return Err(Error::InvalidPolicy); } Ok(()) diff --git a/contracts/smart-account/src/auth/policy/time_based.rs b/contracts/smart-account/src/auth/policy/time_based.rs index b434fac..6ff75e6 100644 --- a/contracts/smart-account/src/auth/policy/time_based.rs +++ b/contracts/smart-account/src/auth/policy/time_based.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{auth::Context, contracttype, Env}; +use soroban_sdk::{auth::Context, contracttype, symbol_short, Env}; use crate::{ auth::permissions::{AuthorizationCheck, PolicyValidator}, @@ -23,9 +23,27 @@ impl PolicyValidator for TimeBasedPolicy { fn check(&self, env: &Env) -> Result<(), Error> { let current_time = env.ledger().timestamp(); if self.not_after < current_time { + env.events().publish( + (symbol_short!("policy"), symbol_short!("failed")), + crate::account::PolicyValidationFailedEvent { + policy_type: soroban_sdk::String::from_str(env, "time_based"), + error_code: 10, + error_message: soroban_sdk::String::from_str(env, "InvalidNotAfterTime"), + signer_key: None, + }, + ); return Err(Error::InvalidNotAfterTime); } if self.not_before > self.not_after { + env.events().publish( + (symbol_short!("policy"), symbol_short!("failed")), + crate::account::PolicyValidationFailedEvent { + policy_type: soroban_sdk::String::from_str(env, "time_based"), + error_code: 11, + error_message: soroban_sdk::String::from_str(env, "InvalidTimeRange"), + signer_key: None, + }, + ); return Err(Error::InvalidTimeRange); } Ok(()) diff --git a/contracts/smart-account/src/auth/signers/ed25519.rs b/contracts/smart-account/src/auth/signers/ed25519.rs index a5e1683..331793e 100644 --- a/contracts/smart-account/src/auth/signers/ed25519.rs +++ b/contracts/smart-account/src/auth/signers/ed25519.rs @@ -2,7 +2,7 @@ use crate::auth::proof::SignerProof; use crate::auth::signer::SignerKey; use crate::auth::signers::SignatureVerifier; use crate::error::Error; -use soroban_sdk::{contracttype, Bytes, BytesN, Env}; +use soroban_sdk::{contracttype, symbol_short, Bytes, BytesN, Env}; /// Ed25519 signer implementation #[contracttype] @@ -29,7 +29,18 @@ impl SignatureVerifier for Ed25519Signer { ); Ok(()) } - SignerProof::Secp256r1(_) => Err(Error::InvalidProofType), + SignerProof::Secp256r1(_) => { + env.events().publish( + (symbol_short!("sig"), symbol_short!("failed")), + crate::account::SignatureVerificationFailedEvent { + error_code: 7, + error_message: soroban_sdk::String::from_str(env, "InvalidProofType"), + signer_key: soroban_sdk::String::from_str(env, "ed25519_key"), + proof_type: soroban_sdk::String::from_str(env, "secp256r1_mismatch"), + }, + ); + Err(Error::InvalidProofType) + } } } } diff --git a/contracts/smart-account/src/auth/signers/secp256r1.rs b/contracts/smart-account/src/auth/signers/secp256r1.rs index a26775d..677481c 100644 --- a/contracts/smart-account/src/auth/signers/secp256r1.rs +++ b/contracts/smart-account/src/auth/signers/secp256r1.rs @@ -2,7 +2,7 @@ use crate::auth::proof::{Secp256r1Signature, SignerProof}; use crate::auth::signer::SignerKey; use crate::auth::signers::SignatureVerifier; use crate::error::Error; -use soroban_sdk::{contracttype, Bytes, BytesN, Env}; +use soroban_sdk::{contracttype, symbol_short, Bytes, BytesN, Env}; #[contracttype] #[derive(Clone, Debug, PartialEq)] @@ -38,7 +38,18 @@ impl SignatureVerifier for Secp256r1Signer { Ok(()) } - _ => Err(Error::InvalidProofType), + _ => { + env.events().publish( + (symbol_short!("sig"), symbol_short!("failed")), + crate::account::SignatureVerificationFailedEvent { + error_code: 7, + error_message: soroban_sdk::String::from_str(env, "InvalidProofType"), + signer_key: soroban_sdk::String::from_str(env, "secp256r1_key"), + proof_type: soroban_sdk::String::from_str(env, "proof_type_mismatch"), + }, + ); + Err(Error::InvalidProofType) + } } } } diff --git a/contracts/smart-account/src/lib.rs b/contracts/smart-account/src/lib.rs index e76b8f3..b94fe4b 100644 --- a/contracts/smart-account/src/lib.rs +++ b/contracts/smart-account/src/lib.rs @@ -6,6 +6,10 @@ pub mod error; pub mod interface; // Re-export key types for external use and bindings generation +pub use account::{ + AuthCheckFailedEvent, PolicyValidationFailedEvent, SignatureVerificationFailedEvent, + SignerOperationFailedEvent, +}; pub use auth::permissions::{SignerPolicy, SignerRole}; pub use auth::proof::{SignatureProofs, SignerProof}; pub use auth::signer::{Signer, SignerKey};