diff --git a/contracts/teachlink/src/atomic_swap.rs b/contracts/teachlink/src/atomic_swap.rs index f3a6f21..d9f5b0f 100644 --- a/contracts/teachlink/src/atomic_swap.rs +++ b/contracts/teachlink/src/atomic_swap.rs @@ -127,6 +127,14 @@ impl AtomicSwapManager { return Err(BridgeError::InvalidInput); } + // Temporal Validation + crate::validation::TimeValidator::validate_global_bounds(env, env.ledger().timestamp()) + .map_err(|_| BridgeError::InvalidTimestamp)?; + + let future_timelock = env.ledger().timestamp().saturating_add(timelock); + crate::validation::TimeValidator::validate_operational_bounds(env, future_timelock) + .map_err(|_| BridgeError::InvalidTimestamp)?; + let mut swap_counter: u64 = env.storage().instance().get(&SWAP_COUNTER).unwrap_or(0u64); swap_counter += 1; diff --git a/contracts/teachlink/src/audit.rs b/contracts/teachlink/src/audit.rs index 952cbd9..d00c005 100644 --- a/contracts/teachlink/src/audit.rs +++ b/contracts/teachlink/src/audit.rs @@ -82,6 +82,10 @@ impl AuditManager { audit_counter += 1; + // Validate timestamp sanity + crate::validation::TimeValidator::validate_global_bounds(env, env.ledger().timestamp()) + .map_err(|_| BridgeError::InvalidTimestamp)?; + // Create audit record let record = AuditRecord { record_id: audit_counter, diff --git a/contracts/teachlink/src/errors.rs b/contracts/teachlink/src/errors.rs index c4fc4eb..a5378b2 100644 --- a/contracts/teachlink/src/errors.rs +++ b/contracts/teachlink/src/errors.rs @@ -80,4 +80,4 @@ pub enum BridgeError { StorageError = 143, NotInitialized = 144, IncompatibleInterfaceVersion = 145, - InvalidInterfaceVersionRange \ No newline at end of file + InvalidInterfaceVersionRange diff --git a/contracts/teachlink/src/timestamp_tests.rs b/contracts/teachlink/src/timestamp_tests.rs new file mode 100644 index 0000000..792a9c4 --- /dev/null +++ b/contracts/teachlink/src/timestamp_tests.rs @@ -0,0 +1,89 @@ +#[cfg(test)] +mod tests { + use crate::validation::{config, TimeValidator, ValidationError}; + use soroban_sdk::{testutils::Ledger, Env}; + + fn set_ledger_time(env: &Env, timestamp: u64) { + env.ledger().with_mut(|li| { + li.timestamp = timestamp; + }); + } + + #[test] + fn test_global_bounds_pass() { + let env = Env::default(); + let now = 1_000_000; + set_ledger_time(&env, now); + + assert!(TimeValidator::validate_global_bounds(&env, now).is_ok()); + assert!(TimeValidator::validate_global_bounds(&env, now + 3600).is_ok()); + assert!(TimeValidator::validate_global_bounds(&env, now - 3600).is_ok()); + } + + #[test] + fn test_global_bounds_fail_future() { + let env = Env::default(); + let now = 1_000_000; + set_ledger_time(&env, now); + + let way_future = now + config::MAX_TIMEOUT_SECONDS + 1; + assert_eq!( + TimeValidator::validate_global_bounds(&env, way_future), + Err(ValidationError::InvalidTimestamp) + ); + } + + #[test] + fn test_global_bounds_fail_past() { + let env = Env::default(); + let now = config::MAX_TIMEOUT_SECONDS + 1_000_000; + set_ledger_time(&env, now); + + let way_past = 0; + assert_eq!( + TimeValidator::validate_global_bounds(&env, way_past), + Err(ValidationError::InvalidTimestamp) + ); + } + + #[test] + fn test_operational_bounds() { + let env = Env::default(); + let now = 10_000_000; + set_ledger_time(&env, now); + + // 90 days = 7,776,000 seconds + assert!(TimeValidator::validate_operational_bounds(&env, now + 7_000_000).is_ok()); + + assert_eq!( + TimeValidator::validate_operational_bounds(&env, now + 8_000_000), + Err(ValidationError::InvalidTimestamp) + ); + } + + #[test] + fn test_monotonicity() { + assert!(TimeValidator::check_monotonic(100, 101).is_ok()); + assert!(TimeValidator::check_monotonic(100, 100).is_ok()); + assert_eq!( + TimeValidator::check_monotonic(101, 100), + Err(ValidationError::TimestampNotMonotonic) + ); + } + + #[test] + fn test_skew_tolerance() { + let env = Env::default(); + let now = 1_000_000; + set_ledger_time(&env, now); + + // 15 minutes = 900 seconds + assert!(TimeValidator::validate_skew(&env, now + 800).is_ok()); + assert!(TimeValidator::validate_skew(&env, now - 800).is_ok()); + + assert_eq!( + TimeValidator::validate_skew(&env, now + 1000), + Err(ValidationError::TimestampSkewExceeded) + ); + } +} diff --git a/contracts/teachlink/src/validation.rs b/contracts/teachlink/src/validation.rs index bfa3a3b..1cf01cd 100644 --- a/contracts/teachlink/src/validation.rs +++ b/contracts/teachlink/src/validation.rs @@ -58,8 +58,9 @@ pub enum ValidationError { DuplicateSigners, InvalidBytesLength, InvalidCrossChainData, - SelfInteractionNotAllowed, - WhitespaceOnlyString, + InvalidTimestamp, + TimestampNotMonotonic, + TimestampSkewExceeded, } /// Result type for validation operations @@ -335,6 +336,74 @@ impl BytesValidator { } } +/// Time validation utilities +pub struct TimeValidator; + +impl TimeValidator { + /// Validates if a timestamp is within the global sanity bound (10 years) + pub fn validate_global_bounds(env: &Env, timestamp: u64) -> ValidationResult<()> { + let current_time = env.ledger().timestamp(); + + // Prevent far-future timestamps + if timestamp > current_time + config::MAX_TIMEOUT_SECONDS { + return Err(ValidationError::InvalidTimestamp); + } + + // Prevent far-past timestamps (saturating sub for safety) + if timestamp < current_time.saturating_sub(config::MAX_TIMEOUT_SECONDS) { + return Err(ValidationError::InvalidTimestamp); + } + + Ok(()) + } + + /// Validates if a timestamp is within operational bounds (90 days) + pub fn validate_operational_bounds(env: &Env, timestamp: u64) -> ValidationResult<()> { + let current_time = env.ledger().timestamp(); + + if timestamp > current_time + config::MAX_OPERATIONAL_TIMEOUT { + return Err(ValidationError::InvalidTimestamp); + } + + if timestamp < current_time.saturating_sub(config::MAX_OPERATIONAL_TIMEOUT) { + return Err(ValidationError::InvalidTimestamp); + } + + Ok(()) + } + + /// Ensures that time has progressed monotonically + pub fn check_monotonic(last_timestamp: u64, current_timestamp: u64) -> ValidationResult<()> { + if current_timestamp < last_timestamp { + return Err(ValidationError::TimestampNotMonotonic); + } + Ok(()) + } + + /// Validates a timestamp with network skew tolerance (15 minutes) + pub fn validate_skew(env: &Env, external_timestamp: u64) -> ValidationResult<()> { + let current_time = env.ledger().timestamp(); + let diff = if external_timestamp > current_time { + external_timestamp - current_time + } else { + current_time - external_timestamp + }; + + if diff > config::MAX_TIME_SKEW { + return Err(ValidationError::TimestampSkewExceeded); + } + Ok(()) + } + + /// Validates that a deadline is actually in the future + pub fn validate_is_future(env: &Env, deadline: u64) -> ValidationResult<()> { + if deadline <= env.ledger().timestamp() { + return Err(ValidationError::InvalidTimestamp); + } + Ok(()) + } +} + /// Cross-chain data validation utilities pub struct CrossChainValidator; @@ -415,10 +484,23 @@ impl EscrowValidator { } // Validate time constraints + if let Some(release) = release_time { + TimeValidator::validate_global_bounds(env, release) + .map_err(|_| EscrowError::InvalidTimestamp)?; + TimeValidator::validate_is_future(env, release) + .map_err(|_| EscrowError::InvalidTimestamp)?; + } + + if let Some(refund) = refund_time { + TimeValidator::validate_global_bounds(env, refund) + .map_err(|_| EscrowError::InvalidTimestamp)?; + TimeValidator::validate_is_future(env, refund) + .map_err(|_| EscrowError::InvalidTimestamp)?; + } + if let (Some(release), Some(refund)) = (release_time, refund_time) { - if refund <= release { - return Err(EscrowError::RefundTimeMustBeAfterReleaseTime); - } + TimeValidator::check_monotonic(release, refund) + .map_err(|_| EscrowError::RefundTimeMustBeAfterReleaseTime)?; } Ok(()) @@ -606,6 +688,10 @@ impl BridgeValidator { InputSanitizer::sanitize_destination_address(destination_address) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; + // Validate current timestamp sanity + TimeValidator::validate_global_bounds(env, env.ledger().timestamp()) + .map_err(|_| crate::errors::BridgeError::InvalidTimestamp)?; + Ok(()) } @@ -631,6 +717,10 @@ impl BridgeValidator { ) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; + // Validate cross-chain message timestamp sanity (use current ledger time) + TimeValidator::validate_global_bounds(env, env.ledger().timestamp()) + .map_err(|_| crate::errors::BridgeError::InvalidTimestamp)?; + Ok(()) } }