From 533641a94483ffcd7378b6ca85f4658e5cf0f679 Mon Sep 17 00:00:00 2001 From: LEO Date: Wed, 29 Jul 2026 21:57:23 +0000 Subject: [PATCH] feat(refund): add instance TTL bump on hot read/write paths Add bump_instance_ttl helper and call it from all write paths (request_refund, approve_refund, reject_refund, process_refund, update_config) as well as view functions (get_admin, get_config, get_refund_request, get_total_refunds, get_refund_counter) to prevent premature archival of instance storage. Add focused tests for TTL bumps on each write path entrypoint. --- contracts/refund/src/lib.rs | 32 +++---- contracts/refund/src/test_ttl_bump.rs | 120 ++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/contracts/refund/src/lib.rs b/contracts/refund/src/lib.rs index ed40c5c7..a9ce5a4a 100644 --- a/contracts/refund/src/lib.rs +++ b/contracts/refund/src/lib.rs @@ -103,6 +103,7 @@ impl RefundContract { ) -> Result { requester.require_auth(); Self::ensure_initialized(&env)?; + Self::bump_instance_ttl(&env); if amount <= 0 { return Err(RefundError::InvalidAmount); @@ -170,6 +171,7 @@ impl RefundContract { ) -> Result<(), RefundError> { admin.require_auth(); Self::ensure_initialized(&env)?; + Self::bump_instance_ttl(&env); let stored_admin: Address = Self::get_admin_internal(&env)?; if admin != stored_admin { @@ -222,6 +224,7 @@ impl RefundContract { ) -> Result<(), RefundError> { admin.require_auth(); Self::ensure_initialized(&env)?; + Self::bump_instance_ttl(&env); let stored_admin: Address = Self::get_admin_internal(&env)?; if admin != stored_admin { @@ -278,6 +281,7 @@ impl RefundContract { ) -> Result<(), RefundError> { admin.require_auth(); Self::ensure_initialized(&env)?; + Self::bump_instance_ttl(&env); let stored_admin: Address = Self::get_admin_internal(&env)?; if admin != stored_admin { @@ -345,6 +349,7 @@ impl RefundContract { ) -> Result<(), RefundError> { admin.require_auth(); Self::ensure_initialized(&env)?; + Self::bump_instance_ttl(&env); let stored_admin: Address = Self::get_admin_internal(&env)?; if admin != stored_admin { @@ -374,9 +379,7 @@ impl RefundContract { /// /// Bumps instance storage TTL on read to prevent premature archival. pub fn get_admin(env: Env) -> Result { - env.storage() - .instance() - .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::bump_instance_ttl(&env); Self::get_admin_internal(&env) } @@ -384,9 +387,7 @@ impl RefundContract { /// /// Bumps instance storage TTL on read to prevent premature archival. pub fn get_config(env: Env) -> Result { - env.storage() - .instance() - .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::bump_instance_ttl(&env); env.storage() .instance() .get(&StorageKey::RefundConfig) @@ -400,9 +401,7 @@ impl RefundContract { env: Env, request_id: u64, ) -> Result { - env.storage() - .instance() - .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::bump_instance_ttl(&env); let key = StorageKey::PendingRefund(request_id); if env.storage().persistent().has(&key) { @@ -420,9 +419,7 @@ impl RefundContract { /// /// Bumps instance storage TTL on read to prevent premature archival. pub fn get_total_refunds(env: Env) -> Result { - env.storage() - .instance() - .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::bump_instance_ttl(&env); Self::ensure_initialized(&env)?; Ok(env .storage() @@ -435,9 +432,7 @@ impl RefundContract { /// /// Bumps instance storage TTL on read to prevent premature archival. pub fn get_refund_counter(env: Env) -> Result { - env.storage() - .instance() - .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::bump_instance_ttl(&env); Self::ensure_initialized(&env)?; Ok(env .storage() @@ -462,6 +457,13 @@ impl RefundContract { Ok(()) } + /// Bump instance storage TTL to prevent premature archival on hot read paths. + pub(crate) fn bump_instance_ttl(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_BUMP_THRESHOLD, INSTANCE_BUMP_AMOUNT); + } + /// Publish the `"initialized"` event. /// /// **What**: Announces that the contract has just been configured with its diff --git a/contracts/refund/src/test_ttl_bump.rs b/contracts/refund/src/test_ttl_bump.rs index c93782e8..8fe793de 100644 --- a/contracts/refund/src/test_ttl_bump.rs +++ b/contracts/refund/src/test_ttl_bump.rs @@ -184,4 +184,124 @@ fn test_multiple_refund_requests_bump_ttl() { env.storage().instance().get_ttl() }); assert_eq!(inst_ttl, INSTANCE_BUMP_AMOUNT); +} + +#[test] +fn test_request_refund_bumps_instance_ttl() { + let (env, _admin, client) = setup(); + let requester = Address::generate(&env); + let token = Address::generate(&env); + + let seq = env.ledger().sequence(); + env.ledger() + .set_sequence_number(seq + INSTANCE_BUMP_AMOUNT - INSTANCE_BUMP_THRESHOLD + 10); + + let ttl_before = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert!(ttl_before < INSTANCE_BUMP_THRESHOLD); + + let _ = client.request_refund(&requester, &token, &500, &Symbol::new(&env, "test")); + + let ttl_after = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert_eq!(ttl_after, INSTANCE_BUMP_AMOUNT); +} + +#[test] +fn test_approve_refund_bumps_instance_ttl() { + let (env, admin, client) = setup(); + let requester = Address::generate(&env); + let token = Address::generate(&env); + + let request_id = client.request_refund(&requester, &token, &500, &Symbol::new(&env, "test")); + + let seq = env.ledger().sequence(); + env.ledger() + .set_sequence_number(seq + INSTANCE_BUMP_AMOUNT - INSTANCE_BUMP_THRESHOLD + 10); + + let ttl_before = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert!(ttl_before < INSTANCE_BUMP_THRESHOLD); + + client.approve_refund(&admin, &request_id); + + let ttl_after = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert_eq!(ttl_after, INSTANCE_BUMP_AMOUNT); +} + +#[test] +fn test_reject_refund_bumps_instance_ttl() { + let (env, admin, client) = setup(); + let requester = Address::generate(&env); + let token = Address::generate(&env); + + let request_id = client.request_refund(&requester, &token, &500, &Symbol::new(&env, "test")); + + let seq = env.ledger().sequence(); + env.ledger() + .set_sequence_number(seq + INSTANCE_BUMP_AMOUNT - INSTANCE_BUMP_THRESHOLD + 10); + + let ttl_before = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert!(ttl_before < INSTANCE_BUMP_THRESHOLD); + + client.reject_refund(&admin, &request_id); + + let ttl_after = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert_eq!(ttl_after, INSTANCE_BUMP_AMOUNT); +} + +#[test] +fn test_process_refund_bumps_instance_ttl() { + let (env, admin, client) = setup(); + let requester = Address::generate(&env); + let token = Address::generate(&env); + + let request_id = client.request_refund(&requester, &token, &500, &Symbol::new(&env, "test")); + client.approve_refund(&admin, &request_id); + + let seq = env.ledger().sequence(); + env.ledger() + .set_sequence_number(seq + INSTANCE_BUMP_AMOUNT - INSTANCE_BUMP_THRESHOLD + 10); + + let ttl_before = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert!(ttl_before < INSTANCE_BUMP_THRESHOLD); + + client.process_refund(&admin, &request_id); + + let ttl_after = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert_eq!(ttl_after, INSTANCE_BUMP_AMOUNT); +} + +#[test] +fn test_update_config_bumps_instance_ttl() { + let (env, admin, client) = setup(); + + let seq = env.ledger().sequence(); + env.ledger() + .set_sequence_number(seq + INSTANCE_BUMP_AMOUNT - INSTANCE_BUMP_THRESHOLD + 10); + + let ttl_before = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert!(ttl_before < INSTANCE_BUMP_THRESHOLD); + + client.update_config(&admin, &500, &200); + + let ttl_after = env.as_contract(&env.current_contract_address(), || { + env.storage().instance().get_ttl() + }); + assert_eq!(ttl_after, INSTANCE_BUMP_AMOUNT); } \ No newline at end of file