From a232b5cb2ce754905d85ca8b2836b7f7edd8b41e Mon Sep 17 00:00:00 2001 From: Airstarr Date: Fri, 24 Apr 2026 02:43:36 +0100 Subject: [PATCH 1/3] feat: initialize TeachLink project structure with core modules and error definitions --- contracts/teachlink/src/atomic_swap.rs | 8 ++ contracts/teachlink/src/audit.rs | 4 + contracts/teachlink/src/errors.rs | 2 + contracts/teachlink/src/lib.rs | 1 + contracts/teachlink/src/timestamp_tests.rs | 89 ++++++++++++++++++ contracts/teachlink/src/validation.rs | 102 ++++++++++++++++++++- 6 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 contracts/teachlink/src/timestamp_tests.rs diff --git a/contracts/teachlink/src/atomic_swap.rs b/contracts/teachlink/src/atomic_swap.rs index 98087377..03debbfa 100644 --- a/contracts/teachlink/src/atomic_swap.rs +++ b/contracts/teachlink/src/atomic_swap.rs @@ -78,6 +78,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 42b9de83..4ededdca 100644 --- a/contracts/teachlink/src/audit.rs +++ b/contracts/teachlink/src/audit.rs @@ -38,6 +38,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 80f57cc8..d4223b6d 100644 --- a/contracts/teachlink/src/errors.rs +++ b/contracts/teachlink/src/errors.rs @@ -61,6 +61,7 @@ pub enum BridgeError { IncompatibleInterfaceVersion = 145, InvalidInterfaceVersionRange = 146, ReentrancyDetected = 147, + InvalidTimestamp = 148, } /// Escrow module errors @@ -96,6 +97,7 @@ pub enum EscrowError { InvalidArbitrator = 224, DepositorCannotBeBeneficiary = 225, ReentrancyDetected = 227, + InvalidTimestamp = 228, } /// Rewards module errors diff --git a/contracts/teachlink/src/lib.rs b/contracts/teachlink/src/lib.rs index 3e888a4f..d3cff0c3 100644 --- a/contracts/teachlink/src/lib.rs +++ b/contracts/teachlink/src/lib.rs @@ -138,6 +138,7 @@ mod types; pub mod validation; // TODO: Fix property_based_tests module (requires test dependencies) // pub mod property_based_tests; +mod timestamp_tests; pub use crate::types::{ ColorBlindMode, ComponentConfig, DeviceInfo, FeedbackCategory, FocusStyle, FontSize, diff --git a/contracts/teachlink/src/timestamp_tests.rs b/contracts/teachlink/src/timestamp_tests.rs new file mode 100644 index 00000000..c22018dc --- /dev/null +++ b/contracts/teachlink/src/timestamp_tests.rs @@ -0,0 +1,89 @@ +#[cfg(test)] +mod tests { + use crate::validation::{TimeValidator, ValidationError, config}; + use soroban_sdk::{Env, testutils::Ledger}; + + 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 5afdb33a..1835f996 100644 --- a/contracts/teachlink/src/validation.rs +++ b/contracts/teachlink/src/validation.rs @@ -14,7 +14,9 @@ pub mod config { pub const MAX_CHAIN_ID: u32 = 999999; pub const MAX_ESCROW_DESCRIPTION_LENGTH: u32 = 1000; pub const MIN_TIMEOUT_SECONDS: u64 = 60; // 1 minute minimum - pub const MAX_TIMEOUT_SECONDS: u64 = 31536000 * 10; // 10 years maximum + pub const MAX_TIMEOUT_SECONDS: u64 = 31536000 * 10; // 10 years maximum (global sanity bound) + pub const MAX_OPERATIONAL_TIMEOUT: u64 = 3600 * 24 * 90; // 90 days (for bridge/swaps) + pub const MAX_TIME_SKEW: u64 = 900; // 15 minutes tolerance pub const MAX_PAYLOAD_SIZE: u32 = 4096; // 4 KB max packet payload /// Bridge-specific amount bounds pub const MIN_BRIDGE_AMOUNT: i128 = 1; @@ -36,6 +38,9 @@ pub enum ValidationError { DuplicateSigners, InvalidBytesLength, InvalidCrossChainData, + InvalidTimestamp, + TimestampNotMonotonic, + TimestampSkewExceeded, } /// Result type for validation operations @@ -230,6 +235,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; @@ -304,10 +377,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)?; } // Check for duplicate signers @@ -480,6 +566,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(()) } @@ -505,6 +595,10 @@ impl BridgeValidator { ) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; + // Validate cross-chain message timestamp sanity + TimeValidator::validate_global_bounds(env, message.timestamp) + .map_err(|_| crate::errors::BridgeError::InvalidTimestamp)?; + Ok(()) } } From ba6f156ed10d0f87595b3fd761e2e8d8bf1e5474 Mon Sep 17 00:00:00 2001 From: Airstarr Date: Fri, 24 Apr 2026 10:51:18 +0100 Subject: [PATCH 2/3] feat: implement validation modules, atomic swap logic, and timestamp testing utilities --- contracts/teachlink/src/atomic_swap.rs | 2 +- contracts/teachlink/src/timestamp_tests.rs | 18 +++++++++--------- contracts/teachlink/src/validation.rs | 16 ++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/teachlink/src/atomic_swap.rs b/contracts/teachlink/src/atomic_swap.rs index 03debbfa..7484cce7 100644 --- a/contracts/teachlink/src/atomic_swap.rs +++ b/contracts/teachlink/src/atomic_swap.rs @@ -81,7 +81,7 @@ impl AtomicSwapManager { // 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)?; diff --git a/contracts/teachlink/src/timestamp_tests.rs b/contracts/teachlink/src/timestamp_tests.rs index c22018dc..792a9c46 100644 --- a/contracts/teachlink/src/timestamp_tests.rs +++ b/contracts/teachlink/src/timestamp_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { - use crate::validation::{TimeValidator, ValidationError, config}; - use soroban_sdk::{Env, testutils::Ledger}; + 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| { @@ -14,7 +14,7 @@ mod tests { 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()); @@ -25,7 +25,7 @@ mod tests { 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), @@ -38,7 +38,7 @@ mod tests { 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), @@ -51,10 +51,10 @@ mod tests { 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) @@ -76,11 +76,11 @@ mod tests { 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 1835f996..5c537e16 100644 --- a/contracts/teachlink/src/validation.rs +++ b/contracts/teachlink/src/validation.rs @@ -242,32 +242,32 @@ 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(()) } @@ -287,13 +287,13 @@ impl TimeValidator { } 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() { From 18e7efffc8062f50c4fbffedebbad11b314ce502 Mon Sep 17 00:00:00 2001 From: Airstarr Date: Fri, 24 Apr 2026 11:11:47 +0100 Subject: [PATCH 3/3] fix: update timestamp validation to use current ledger time --- contracts/teachlink/src/validation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/teachlink/src/validation.rs b/contracts/teachlink/src/validation.rs index 5c537e16..3e06f368 100644 --- a/contracts/teachlink/src/validation.rs +++ b/contracts/teachlink/src/validation.rs @@ -595,8 +595,8 @@ impl BridgeValidator { ) .map_err(|_| crate::errors::BridgeError::InvalidInput)?; - // Validate cross-chain message timestamp sanity - TimeValidator::validate_global_bounds(env, message.timestamp) + // 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(())