Skip to content

Commit b51bad3

Browse files
authored
Merge pull request #410 from Airstarr/timestamp
feat: initialize TeachLink project structure with core modules and error definitions
2 parents 8a29827 + b316a79 commit b51bad3

5 files changed

Lines changed: 197 additions & 6 deletions

File tree

contracts/teachlink/src/atomic_swap.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ impl AtomicSwapManager {
127127
return Err(BridgeError::InvalidInput);
128128
}
129129

130+
// Temporal Validation
131+
crate::validation::TimeValidator::validate_global_bounds(env, env.ledger().timestamp())
132+
.map_err(|_| BridgeError::InvalidTimestamp)?;
133+
134+
let future_timelock = env.ledger().timestamp().saturating_add(timelock);
135+
crate::validation::TimeValidator::validate_operational_bounds(env, future_timelock)
136+
.map_err(|_| BridgeError::InvalidTimestamp)?;
137+
130138
let mut swap_counter: u64 = env.storage().instance().get(&SWAP_COUNTER).unwrap_or(0u64);
131139
swap_counter += 1;
132140

contracts/teachlink/src/audit.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ impl AuditManager {
8282

8383
audit_counter += 1;
8484

85+
// Validate timestamp sanity
86+
crate::validation::TimeValidator::validate_global_bounds(env, env.ledger().timestamp())
87+
.map_err(|_| BridgeError::InvalidTimestamp)?;
88+
8589
// Create audit record
8690
let record = AuditRecord {
8791
record_id: audit_counter,

contracts/teachlink/src/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ pub enum BridgeError {
8080
StorageError = 143,
8181
NotInitialized = 144,
8282
IncompatibleInterfaceVersion = 145,
83-
InvalidInterfaceVersionRange
83+
InvalidInterfaceVersionRange
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use crate::validation::{config, TimeValidator, ValidationError};
4+
use soroban_sdk::{testutils::Ledger, Env};
5+
6+
fn set_ledger_time(env: &Env, timestamp: u64) {
7+
env.ledger().with_mut(|li| {
8+
li.timestamp = timestamp;
9+
});
10+
}
11+
12+
#[test]
13+
fn test_global_bounds_pass() {
14+
let env = Env::default();
15+
let now = 1_000_000;
16+
set_ledger_time(&env, now);
17+
18+
assert!(TimeValidator::validate_global_bounds(&env, now).is_ok());
19+
assert!(TimeValidator::validate_global_bounds(&env, now + 3600).is_ok());
20+
assert!(TimeValidator::validate_global_bounds(&env, now - 3600).is_ok());
21+
}
22+
23+
#[test]
24+
fn test_global_bounds_fail_future() {
25+
let env = Env::default();
26+
let now = 1_000_000;
27+
set_ledger_time(&env, now);
28+
29+
let way_future = now + config::MAX_TIMEOUT_SECONDS + 1;
30+
assert_eq!(
31+
TimeValidator::validate_global_bounds(&env, way_future),
32+
Err(ValidationError::InvalidTimestamp)
33+
);
34+
}
35+
36+
#[test]
37+
fn test_global_bounds_fail_past() {
38+
let env = Env::default();
39+
let now = config::MAX_TIMEOUT_SECONDS + 1_000_000;
40+
set_ledger_time(&env, now);
41+
42+
let way_past = 0;
43+
assert_eq!(
44+
TimeValidator::validate_global_bounds(&env, way_past),
45+
Err(ValidationError::InvalidTimestamp)
46+
);
47+
}
48+
49+
#[test]
50+
fn test_operational_bounds() {
51+
let env = Env::default();
52+
let now = 10_000_000;
53+
set_ledger_time(&env, now);
54+
55+
// 90 days = 7,776,000 seconds
56+
assert!(TimeValidator::validate_operational_bounds(&env, now + 7_000_000).is_ok());
57+
58+
assert_eq!(
59+
TimeValidator::validate_operational_bounds(&env, now + 8_000_000),
60+
Err(ValidationError::InvalidTimestamp)
61+
);
62+
}
63+
64+
#[test]
65+
fn test_monotonicity() {
66+
assert!(TimeValidator::check_monotonic(100, 101).is_ok());
67+
assert!(TimeValidator::check_monotonic(100, 100).is_ok());
68+
assert_eq!(
69+
TimeValidator::check_monotonic(101, 100),
70+
Err(ValidationError::TimestampNotMonotonic)
71+
);
72+
}
73+
74+
#[test]
75+
fn test_skew_tolerance() {
76+
let env = Env::default();
77+
let now = 1_000_000;
78+
set_ledger_time(&env, now);
79+
80+
// 15 minutes = 900 seconds
81+
assert!(TimeValidator::validate_skew(&env, now + 800).is_ok());
82+
assert!(TimeValidator::validate_skew(&env, now - 800).is_ok());
83+
84+
assert_eq!(
85+
TimeValidator::validate_skew(&env, now + 1000),
86+
Err(ValidationError::TimestampSkewExceeded)
87+
);
88+
}
89+
}

contracts/teachlink/src/validation.rs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ pub enum ValidationError {
5858
DuplicateSigners,
5959
InvalidBytesLength,
6060
InvalidCrossChainData,
61-
SelfInteractionNotAllowed,
62-
WhitespaceOnlyString,
61+
InvalidTimestamp,
62+
TimestampNotMonotonic,
63+
TimestampSkewExceeded,
6364
}
6465

6566
/// Result type for validation operations
@@ -335,6 +336,74 @@ impl BytesValidator {
335336
}
336337
}
337338

339+
/// Time validation utilities
340+
pub struct TimeValidator;
341+
342+
impl TimeValidator {
343+
/// Validates if a timestamp is within the global sanity bound (10 years)
344+
pub fn validate_global_bounds(env: &Env, timestamp: u64) -> ValidationResult<()> {
345+
let current_time = env.ledger().timestamp();
346+
347+
// Prevent far-future timestamps
348+
if timestamp > current_time + config::MAX_TIMEOUT_SECONDS {
349+
return Err(ValidationError::InvalidTimestamp);
350+
}
351+
352+
// Prevent far-past timestamps (saturating sub for safety)
353+
if timestamp < current_time.saturating_sub(config::MAX_TIMEOUT_SECONDS) {
354+
return Err(ValidationError::InvalidTimestamp);
355+
}
356+
357+
Ok(())
358+
}
359+
360+
/// Validates if a timestamp is within operational bounds (90 days)
361+
pub fn validate_operational_bounds(env: &Env, timestamp: u64) -> ValidationResult<()> {
362+
let current_time = env.ledger().timestamp();
363+
364+
if timestamp > current_time + config::MAX_OPERATIONAL_TIMEOUT {
365+
return Err(ValidationError::InvalidTimestamp);
366+
}
367+
368+
if timestamp < current_time.saturating_sub(config::MAX_OPERATIONAL_TIMEOUT) {
369+
return Err(ValidationError::InvalidTimestamp);
370+
}
371+
372+
Ok(())
373+
}
374+
375+
/// Ensures that time has progressed monotonically
376+
pub fn check_monotonic(last_timestamp: u64, current_timestamp: u64) -> ValidationResult<()> {
377+
if current_timestamp < last_timestamp {
378+
return Err(ValidationError::TimestampNotMonotonic);
379+
}
380+
Ok(())
381+
}
382+
383+
/// Validates a timestamp with network skew tolerance (15 minutes)
384+
pub fn validate_skew(env: &Env, external_timestamp: u64) -> ValidationResult<()> {
385+
let current_time = env.ledger().timestamp();
386+
let diff = if external_timestamp > current_time {
387+
external_timestamp - current_time
388+
} else {
389+
current_time - external_timestamp
390+
};
391+
392+
if diff > config::MAX_TIME_SKEW {
393+
return Err(ValidationError::TimestampSkewExceeded);
394+
}
395+
Ok(())
396+
}
397+
398+
/// Validates that a deadline is actually in the future
399+
pub fn validate_is_future(env: &Env, deadline: u64) -> ValidationResult<()> {
400+
if deadline <= env.ledger().timestamp() {
401+
return Err(ValidationError::InvalidTimestamp);
402+
}
403+
Ok(())
404+
}
405+
}
406+
338407
/// Cross-chain data validation utilities
339408
pub struct CrossChainValidator;
340409

@@ -415,10 +484,23 @@ impl EscrowValidator {
415484
}
416485

417486
// Validate time constraints
487+
if let Some(release) = release_time {
488+
TimeValidator::validate_global_bounds(env, release)
489+
.map_err(|_| EscrowError::InvalidTimestamp)?;
490+
TimeValidator::validate_is_future(env, release)
491+
.map_err(|_| EscrowError::InvalidTimestamp)?;
492+
}
493+
494+
if let Some(refund) = refund_time {
495+
TimeValidator::validate_global_bounds(env, refund)
496+
.map_err(|_| EscrowError::InvalidTimestamp)?;
497+
TimeValidator::validate_is_future(env, refund)
498+
.map_err(|_| EscrowError::InvalidTimestamp)?;
499+
}
500+
418501
if let (Some(release), Some(refund)) = (release_time, refund_time) {
419-
if refund <= release {
420-
return Err(EscrowError::RefundTimeMustBeAfterReleaseTime);
421-
}
502+
TimeValidator::check_monotonic(release, refund)
503+
.map_err(|_| EscrowError::RefundTimeMustBeAfterReleaseTime)?;
422504
}
423505

424506
Ok(())
@@ -606,6 +688,10 @@ impl BridgeValidator {
606688
InputSanitizer::sanitize_destination_address(destination_address)
607689
.map_err(|_| crate::errors::BridgeError::InvalidInput)?;
608690

691+
// Validate current timestamp sanity
692+
TimeValidator::validate_global_bounds(env, env.ledger().timestamp())
693+
.map_err(|_| crate::errors::BridgeError::InvalidTimestamp)?;
694+
609695
Ok(())
610696
}
611697

@@ -631,6 +717,10 @@ impl BridgeValidator {
631717
)
632718
.map_err(|_| crate::errors::BridgeError::InvalidInput)?;
633719

720+
// Validate cross-chain message timestamp sanity (use current ledger time)
721+
TimeValidator::validate_global_bounds(env, env.ledger().timestamp())
722+
.map_err(|_| crate::errors::BridgeError::InvalidTimestamp)?;
723+
634724
Ok(())
635725
}
636726
}

0 commit comments

Comments
 (0)