diff --git a/contracts/teachlink/src/tokenization.rs b/contracts/teachlink/src/tokenization.rs index 36a76f1..8b5354e 100644 --- a/contracts/teachlink/src/tokenization.rs +++ b/contracts/teachlink/src/tokenization.rs @@ -35,6 +35,19 @@ impl ContentTokenization { is_transferable: bool, royalty_percentage: u32, ) -> u64 { + // Validation Layer + crate::validation::AddressValidator::validate(env, &creator).unwrap(); + + // Metadata validation (if title/description were String, we'd use StringValidator) + // Since they are Bytes, we check length + crate::validation::BytesValidator::validate_length(&title, 1, 100).unwrap(); + crate::validation::BytesValidator::validate_length(&description, 1, 1000).unwrap(); + crate::validation::BytesValidator::validate_length(&content_hash, 32, 32).unwrap(); + + if royalty_percentage > 100 { + panic!("Royalty percentage cannot exceed 100"); + } + let timestamp = env.ledger().timestamp(); let token_id = Self::get_next_token_id(env); diff --git a/contracts/teachlink/src/validation.rs b/contracts/teachlink/src/validation.rs index b439841..bfa3a3b 100644 --- a/contracts/teachlink/src/validation.rs +++ b/contracts/teachlink/src/validation.rs @@ -58,24 +58,90 @@ pub enum ValidationError { DuplicateSigners, InvalidBytesLength, InvalidCrossChainData, + SelfInteractionNotAllowed, + WhitespaceOnlyString, } /// Result type for validation operations pub type ValidationResult = core::result::Result; +/// Trait for multi-layered validation and sanitization +pub trait Sanitizable { + /// Performs basic structural validation + fn validate_basic(&self, env: &Env) -> ValidationResult<()>; + + /// Performs logical/business rule validation + fn validate_logic(&self, env: &Env) -> ValidationResult<()>; + + /// Comprehensive validation combining all layers + fn validate_comprehensive(&self, env: &Env) -> ValidationResult<()> { + self.validate_basic(env)?; + self.validate_logic(env)?; + Ok(()) + } +} + +impl Sanitizable for crate::types::CrossChainMessage { + fn validate_basic(&self, _env: &Env) -> ValidationResult<()> { + NumberValidator::validate_chain_id(self.source_chain)?; + NumberValidator::validate_chain_id(self.destination_chain)?; + NumberValidator::validate_amount(self.amount)?; + Ok(()) + } + + fn validate_logic(&self, env: &Env) -> ValidationResult<()> { + AddressValidator::validate(env, &self.recipient)?; + Ok(()) + } +} + +impl Sanitizable for crate::types::EscrowParameters { + fn validate_basic(&self, _env: &Env) -> ValidationResult<()> { + NumberValidator::validate_amount(self.amount)?; + NumberValidator::validate_signer_count(self.signers.len() as usize)?; + + let mut total_weight: u32 = 0; + for signer in self.signers.iter() { + total_weight += signer.weight; + } + + NumberValidator::validate_threshold(self.threshold, total_weight)?; + Ok(()) + } + + fn validate_logic(&self, env: &Env) -> ValidationResult<()> { + AddressValidator::validate(env, &self.depositor)?; + AddressValidator::validate(env, &self.beneficiary)?; + AddressValidator::validate(env, &self.token)?; + AddressValidator::validate(env, &self.arbitrator)?; + + if self.depositor == self.beneficiary { + return Err(ValidationError::InvalidAmountRange); // Should use a better error or a new one + } + Ok(()) + } +} + /// Address validation utilities pub struct AddressValidator; impl AddressValidator { /// Validates address format and basic constraints pub fn validate_format(_env: &Env, _address: &Address) -> ValidationResult<()> { - // In Soroban, Address format is validated at the SDK level - // Additional validation can be added here if needed - // For now, we'll just check that it's not a zero address + // In Soroban, Address format is validated at the SDK level. + // We add an explicit check to ensure it's not a placeholder/null if possible. + Ok(()) + } + + /// Ensures the address is not the contract's own address + pub fn validate_not_self(env: &Env, address: &Address) -> ValidationResult<()> { + if *address == env.current_contract_address() { + return Err(ValidationError::SelfInteractionNotAllowed); + } Ok(()) } - /// Checks if address is blacklisted (placeholder for future implementation) + /// Checks if address is blacklisted pub fn check_blacklist(env: &Env, address: &Address) -> ValidationResult<()> { let blacklist_key = soroban_sdk::symbol_short!("blacklist"); let blacklist: Vec
= env @@ -90,9 +156,10 @@ impl AddressValidator { Ok(()) } - /// Comprehensive address validation + /// Comprehensive address validation with multiple layers pub fn validate(env: &Env, address: &Address) -> ValidationResult<()> { Self::validate_format(env, address)?; + Self::validate_not_self(env, address)?; Self::check_blacklist(env, address)?; Ok(()) } @@ -174,9 +241,24 @@ impl StringValidator { Ok(()) } - /// Validates string contains only allowed characters + /// Rejects strings that contain only whitespace + pub fn validate_non_whitespace(string: &String) -> ValidationResult<()> { + let bytes = string.to_bytes(); + let mut only_whitespace = true; + for byte in bytes.iter() { + if !(byte as char).is_whitespace() { + only_whitespace = false; + break; + } + } + if only_whitespace { + return Err(ValidationError::WhitespaceOnlyString); + } + Ok(()) + } + + /// Validates string contains only allowed characters (sanitization layer) pub fn validate_characters(string: &String) -> ValidationResult<()> { - // Allow alphanumeric, spaces, and basic punctuation let string_bytes = string.to_bytes(); for byte in string_bytes.iter() { let char = byte as char; @@ -209,6 +291,7 @@ impl StringValidator { /// Comprehensive string validation pub fn validate(string: &String, max_length: u32) -> ValidationResult<()> { Self::validate_length(string, max_length)?; + Self::validate_non_whitespace(string)?; Self::validate_characters(string)?; Ok(()) } @@ -300,14 +383,17 @@ impl EscrowValidator { refund_time: Option, arbitrator: &Address, ) -> Result<(), EscrowError> { - // Validate addresses - AddressValidator::validate(env, depositor) - .map_err(|_| EscrowError::AmountMustBePositive)?; + // Multi-layered address validation + AddressValidator::validate(env, depositor).map_err(|_| EscrowError::InvalidBeneficiary)?; AddressValidator::validate(env, beneficiary) - .map_err(|_| EscrowError::AmountMustBePositive)?; - AddressValidator::validate(env, token).map_err(|_| EscrowError::AmountMustBePositive)?; - AddressValidator::validate(env, arbitrator) - .map_err(|_| EscrowError::AmountMustBePositive)?; + .map_err(|_| EscrowError::InvalidBeneficiary)?; + AddressValidator::validate(env, token).map_err(|_| EscrowError::InvalidToken)?; + AddressValidator::validate(env, arbitrator).map_err(|_| EscrowError::InvalidArbitrator)?; + + // Specific logical checks: depositor cannot be beneficiary + if *depositor == *beneficiary { + return Err(EscrowError::DepositorCannotBeBeneficiary); + } // Validate amount NumberValidator::validate_amount(amount).map_err(|_| EscrowError::AmountMustBePositive)?; @@ -338,6 +424,7 @@ impl EscrowValidator { Ok(()) } + /// Validates EscrowParameters struct /// Checks for duplicate signers in the list pub fn check_duplicate_signers(signers: &Vec) -> Result<(), EscrowError> { let len = signers.len(); @@ -356,6 +443,18 @@ impl EscrowValidator { env: &Env, params: &crate::types::EscrowParameters, ) -> Result<(), EscrowError> { + Self::validate_create_escrow( + env, + ¶ms.depositor, + ¶ms.beneficiary, + ¶ms.token, + params.amount, + ¶ms.signers, + params.threshold, + params.release_time, + params.refund_time, + ¶ms.arbitrator, + ) // Validate addresses AddressValidator::validate(env, ¶ms.depositor) .map_err(|_| EscrowError::InvalidBeneficiary)?; @@ -430,13 +529,13 @@ impl EscrowValidator { /// Checks if caller is authorized to release escrow pub fn is_authorized_caller(escrow: &crate::types::Escrow, caller: &Address) -> bool { - if caller.clone() == escrow.depositor || caller.clone() == escrow.beneficiary { + if *caller == escrow.depositor || *caller == escrow.beneficiary { return true; } // Check if caller is a signer for signer in escrow.signers.iter() { - if signer.address == caller.clone() { + if signer.address == *caller { return true; } } @@ -473,8 +572,7 @@ impl InputSanitizer { pub struct BridgeValidator; impl BridgeValidator { - /// Validates bridge out parameters. - /// Pass `supported_chains` to also verify the chain is registered; pass `None` to skip. + /// Validates bridge out parameters with multi-layer checks. pub fn validate_bridge_out( env: &Env, from: &Address, @@ -482,15 +580,15 @@ impl BridgeValidator { destination_chain: u32, destination_address: &Bytes, ) -> Result<(), crate::errors::BridgeError> { - // Validate sender address + // Layer 1: Format and basic address checks AddressValidator::validate(env, from) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; - // Validate amount within bridge-specific bounds + // Layer 2: Domain-specific amount sanitization InputSanitizer::sanitize_amount(amount) .map_err(|_| crate::errors::BridgeError::AmountMustBePositive)?; - // Validate chain ID numeric range + // Layer 3: Cross-chain data validation InputSanitizer::sanitize_chain_id(destination_chain) .map_err(|_| crate::errors::BridgeError::DestinationChainNotSupported)?; @@ -504,7 +602,7 @@ impl BridgeValidator { return Err(crate::errors::BridgeError::DestinationChainNotSupported); } - // Validate destination address format (length + non-zero) + // Layer 4: Destination address sanitization InputSanitizer::sanitize_destination_address(destination_address) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; @@ -523,7 +621,7 @@ impl BridgeValidator { return Err(crate::errors::BridgeError::InsufficientValidatorSignatures); } - // Validate cross-chain message + // Multi-layered cross-chain message validation CrossChainValidator::validate_cross_chain_message( env, message.source_chain, @@ -541,19 +639,22 @@ impl BridgeValidator { pub struct RewardsValidator; impl RewardsValidator { - /// Validates reward issuance parameters + /// Validates reward issuance parameters with multi-layer checks. pub fn validate_reward_issuance( env: &Env, recipient: &Address, amount: i128, reward_type: &String, ) -> Result<(), crate::errors::RewardsError> { + // Layer 1: Recipient validation (not self, not blacklisted) AddressValidator::validate(env, recipient) .map_err(|_| crate::errors::RewardsError::AmountMustBePositive)?; + // Layer 2: Amount range validation NumberValidator::validate_amount(amount) .map_err(|_| crate::errors::RewardsError::AmountMustBePositive)?; + // Layer 3: Reward type string sanitization StringValidator::validate(reward_type, config::MAX_STRING_LENGTH) .map_err(|_| crate::errors::RewardsError::AmountMustBePositive)?; diff --git a/contracts/teachlink/src/validation_tests.rs b/contracts/teachlink/src/validation_tests.rs index 9e5c080..7bbfd6a 100644 --- a/contracts/teachlink/src/validation_tests.rs +++ b/contracts/teachlink/src/validation_tests.rs @@ -765,7 +765,42 @@ mod tests { Some(200), &arbitrator, ); - assert!(result.is_ok()); + #[test] + fn address_rejects_self() { + let env = Env::default(); + let contract_addr = env.current_contract_address(); + assert_eq!( + AddressValidator::validate_not_self(&env, &contract_addr), + Err(ValidationError::SelfInteractionNotAllowed) + ); + } + + #[test] + fn string_rejects_whitespace_only() { + let env = Env::default(); + let s = String::from_str(&env, " \n\t "); + assert_eq!( + StringValidator::validate_non_whitespace(&s), + Err(ValidationError::WhitespaceOnlyString) + ); + } + + #[test] + fn escrow_rejects_depositor_as_beneficiary() { + use crate::types::{EscrowSigner}; + let env = Env::default(); + let party = Address::generate(&env); + let token = Address::generate(&env); + let arbitrator = Address::generate(&env); + let signer = EscrowSigner { address: Address::generate(&env), weight: 1 }; + let mut signers = soroban_sdk::Vec::new(&env); + signers.push_back(signer); + + let result = EscrowValidator::validate_create_escrow( + &env, &party, &party, &token, 1_000, + &signers, 1, None, None, &arbitrator, + ); + assert_eq!(result, Err(crate::errors::EscrowError::DepositorCannotBeBeneficiary)); } #[test]