Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions contracts/teachlink/src/tokenization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
149 changes: 125 additions & 24 deletions contracts/teachlink/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,90 @@ pub enum ValidationError {
DuplicateSigners,
InvalidBytesLength,
InvalidCrossChainData,
SelfInteractionNotAllowed,
WhitespaceOnlyString,
}

/// Result type for validation operations
pub type ValidationResult<T> = core::result::Result<T, ValidationError>;

/// 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<Address> = env
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -300,14 +383,17 @@ impl EscrowValidator {
refund_time: Option<u64>,
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)?;
Expand Down Expand Up @@ -338,6 +424,7 @@ impl EscrowValidator {
Ok(())
}

/// Validates EscrowParameters struct
/// Checks for duplicate signers in the list
pub fn check_duplicate_signers(signers: &Vec<EscrowSigner>) -> Result<(), EscrowError> {
let len = signers.len();
Expand All @@ -356,6 +443,18 @@ impl EscrowValidator {
env: &Env,
params: &crate::types::EscrowParameters,
) -> Result<(), EscrowError> {
Self::validate_create_escrow(
env,
&params.depositor,
&params.beneficiary,
&params.token,
params.amount,
&params.signers,
params.threshold,
params.release_time,
params.refund_time,
&params.arbitrator,
)
// Validate addresses
AddressValidator::validate(env, &params.depositor)
.map_err(|_| EscrowError::InvalidBeneficiary)?;
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -473,24 +572,23 @@ 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,
amount: i128,
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)?;

Expand All @@ -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)?;

Expand All @@ -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,
Expand All @@ -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)?;

Expand Down
37 changes: 36 additions & 1 deletion contracts/teachlink/src/validation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading