From 8aa391eb8668f502a32b1d9ca0570221160af3b9 Mon Sep 17 00:00:00 2001 From: dimka90 Date: Sun, 19 Jul 2026 10:19:48 +0100 Subject: [PATCH] feat: bump instance/persistent TTL on all contract entry points Add LEDGER_BUMP/LEDGER_THRESHOLD constants (~1 year) and a bump_instance() helper to both campaign-escrow and dispute-resolution storage layers. Every #[contractimpl] function now bumps the instance TTL, and persistent entries (campaign, application, dispute) are extended on both reads and writes via extend_ttl. Closes: #14 --- contracts/campaign-escrow/src/lib.rs | 12 ++++ contracts/campaign-escrow/src/storage.rs | 45 +++++++++----- contracts/campaign-escrow/src/test.rs | 65 +++++++++++++++++---- contracts/dispute-resolution/src/lib.rs | 5 ++ contracts/dispute-resolution/src/storage.rs | 23 ++++++-- 5 files changed, 118 insertions(+), 32 deletions(-) diff --git a/contracts/campaign-escrow/src/lib.rs b/contracts/campaign-escrow/src/lib.rs index 5109a37..b9f5ceb 100644 --- a/contracts/campaign-escrow/src/lib.rs +++ b/contracts/campaign-escrow/src/lib.rs @@ -53,6 +53,7 @@ impl CampaignEscrowContract { storage::set_admin(&env, &admin); storage::set_dispute_contract(&env, &dispute_contract); storage::set_fee_bps(&env, fee_bps); + storage::bump_instance(&env); Ok(()) } @@ -78,6 +79,7 @@ impl CampaignEscrowContract { completion_deadline: u64, metadata_uri: String, ) -> Result { + storage::bump_instance(&env); todo!("design + implement campaign creation — see doc comment above") } @@ -95,6 +97,7 @@ impl CampaignEscrowContract { business: Address, campaign_id: CampaignId, ) -> Result<(), Error> { + storage::bump_instance(&env); business.require_auth(); todo!("design + implement escrow funding — see doc comment above") } @@ -111,6 +114,7 @@ impl CampaignEscrowContract { campaign_id: CampaignId, pitch_uri: String, ) -> Result<(), Error> { + storage::bump_instance(&env); creator.require_auth(); todo!("design + implement creator applications — see doc comment above") } @@ -129,6 +133,7 @@ impl CampaignEscrowContract { creator: Address, payout_amount: i128, ) -> Result<(), Error> { + storage::bump_instance(&env); business.require_auth(); todo!("design + implement creator approval — see doc comment above") } @@ -146,6 +151,7 @@ impl CampaignEscrowContract { campaign_id: CampaignId, proof_uri: String, ) -> Result<(), Error> { + storage::bump_instance(&env); creator.require_auth(); todo!("design + implement proof submission/verification — see doc comment above") } @@ -164,6 +170,7 @@ impl CampaignEscrowContract { campaign_id: CampaignId, creator: Address, ) -> Result<(), Error> { + storage::bump_instance(&env); business.require_auth(); todo!("design + implement payout release — see doc comment above") } @@ -180,6 +187,7 @@ impl CampaignEscrowContract { business: Address, campaign_id: CampaignId, ) -> Result<(), Error> { + storage::bump_instance(&env); business.require_auth(); todo!("design + implement cancellation/refund — see doc comment above") } @@ -199,6 +207,7 @@ impl CampaignEscrowContract { campaign_id: CampaignId, creator: Address, ) -> Result<(), Error> { + storage::bump_instance(&env); todo!("design + implement dispute freeze hook — see doc comment above") } @@ -213,11 +222,13 @@ impl CampaignEscrowContract { creator: Address, creator_bps: i128, ) -> Result<(), Error> { + storage::bump_instance(&env); todo!("design + implement dispute payout resolution — see doc comment above") } /// Read-only lookup of a campaign's current state. pub fn get_campaign(env: Env, campaign_id: CampaignId) -> Result { + storage::bump_instance(&env); storage::get_campaign(&env, campaign_id) } @@ -227,6 +238,7 @@ impl CampaignEscrowContract { campaign_id: CampaignId, creator: Address, ) -> Result { + storage::bump_instance(&env); storage::get_application(&env, campaign_id, &creator) } } diff --git a/contracts/campaign-escrow/src/storage.rs b/contracts/campaign-escrow/src/storage.rs index 174f15e..76bd449 100644 --- a/contracts/campaign-escrow/src/storage.rs +++ b/contracts/campaign-escrow/src/storage.rs @@ -6,12 +6,11 @@ use soroban_sdk::{contracttype, Address, Env}; use crate::error::Error; use crate::types::{Application, Campaign}; -/// Extend persistent entries by roughly this many ledgers on every write -/// (~30 days at 5s/ledger). TODO(contributors): tune once real rent/TTL -/// costs on target networks are benchmarked, and consider a max-TTL bump on -/// read-heavy paths too. -const PERSISTENT_BUMP_LEDGERS: u32 = 518_400; -const PERSISTENT_LIFETIME_THRESHOLD: u32 = 500_000; +/// Extend persistent entries by roughly this many ledgers (~1 year at +/// 5 s/ledger — the maximum the Stellar network allows for a single +/// `extend_ttl` call). +const LEDGER_BUMP: u32 = 535_680; +const LEDGER_THRESHOLD: u32 = 500_000; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -24,6 +23,14 @@ pub enum DataKey { Application(CampaignId, Address), } +/// Bump the instance TTL so metadata (admin, fee, etc.) doesn't expire +/// while the contract is actively being used. +pub fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(LEDGER_THRESHOLD, LEDGER_BUMP); +} + pub fn is_initialized(env: &Env) -> bool { env.storage().instance().has(&DataKey::Admin) } @@ -76,10 +83,15 @@ pub fn next_campaign_id(env: &Env) -> CampaignId { } pub fn get_campaign(env: &Env, id: CampaignId) -> Result { + let key = DataKey::Campaign(id); + let campaign = env.storage() + .persistent() + .get(&key) + .ok_or(Error::CampaignNotFound)?; env.storage() .persistent() - .get(&DataKey::Campaign(id)) - .ok_or(Error::CampaignNotFound) + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); + Ok(campaign) } pub fn set_campaign(env: &Env, campaign: &Campaign) { @@ -87,8 +99,8 @@ pub fn set_campaign(env: &Env, campaign: &Campaign) { env.storage().persistent().set(&key, campaign); env.storage().persistent().extend_ttl( &key, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_LEDGERS, + LEDGER_THRESHOLD, + LEDGER_BUMP, ); } @@ -97,10 +109,15 @@ pub fn get_application( campaign_id: CampaignId, creator: &Address, ) -> Result { + let key = DataKey::Application(campaign_id, creator.clone()); + let app = env.storage() + .persistent() + .get(&key) + .ok_or(Error::ApplicationNotFound)?; env.storage() .persistent() - .get(&DataKey::Application(campaign_id, creator.clone())) - .ok_or(Error::ApplicationNotFound) + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); + Ok(app) } pub fn set_application(env: &Env, application: &Application) { @@ -108,7 +125,7 @@ pub fn set_application(env: &Env, application: &Application) { env.storage().persistent().set(&key, application); env.storage().persistent().extend_ttl( &key, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_LEDGERS, + LEDGER_THRESHOLD, + LEDGER_BUMP, ); } diff --git a/contracts/campaign-escrow/src/test.rs b/contracts/campaign-escrow/src/test.rs index 4eeb308..f5a155f 100644 --- a/contracts/campaign-escrow/src/test.rs +++ b/contracts/campaign-escrow/src/test.rs @@ -8,19 +8,21 @@ use super::*; use soroban_sdk::testutils::Address as _; use soroban_sdk::Env; -fn setup(env: &Env) -> (CampaignEscrowContractClient<'_>, Address, Address) { +fn setup( + env: &Env, +) -> (CampaignEscrowContractClient<'_>, Address, Address, Address) { + env.mock_all_auths(); let contract_id = env.register(CampaignEscrowContract, ()); let client = CampaignEscrowContractClient::new(env, &contract_id); let admin = Address::generate(env); let dispute_contract = Address::generate(env); - (client, admin, dispute_contract) + (client, contract_id, admin, dispute_contract) } #[test] fn initialize_sets_admin_and_fee() { let env = Env::default(); - env.mock_all_auths(); - let (client, admin, dispute_contract) = setup(&env); + let (client, _contract_id, admin, dispute_contract) = setup(&env); client.initialize(&admin, &dispute_contract, &250); } @@ -28,8 +30,7 @@ fn initialize_sets_admin_and_fee() { #[test] fn initialize_twice_fails() { let env = Env::default(); - env.mock_all_auths(); - let (client, admin, dispute_contract) = setup(&env); + let (client, _contract_id, admin, dispute_contract) = setup(&env); client.initialize(&admin, &dispute_contract, &250); let result = client.try_initialize(&admin, &dispute_contract, &250); @@ -39,8 +40,7 @@ fn initialize_twice_fails() { #[test] fn initialize_rejects_out_of_range_fee() { let env = Env::default(); - env.mock_all_auths(); - let (client, admin, dispute_contract) = setup(&env); + let (client, _contract_id, admin, dispute_contract) = setup(&env); let result = client.try_initialize( &admin, @@ -53,14 +53,56 @@ fn initialize_rejects_out_of_range_fee() { #[test] fn get_campaign_not_found_before_creation() { let env = Env::default(); - env.mock_all_auths(); - let (client, admin, dispute_contract) = setup(&env); + let (client, _contract_id, admin, dispute_contract) = setup(&env); client.initialize(&admin, &dispute_contract, &250); let result = client.try_get_campaign(&0); assert_eq!(result, Err(Ok(Error::CampaignNotFound))); } +#[test] +fn initialize_bumps_instance_ttl() { + let env = Env::default(); + let (client, contract_id, admin, dispute_contract) = setup(&env); + client.initialize(&admin, &dispute_contract, &250); + + let admin_back = env.as_contract(&contract_id, || super::storage::get_admin(&env)); + assert_eq!(admin_back, Ok(admin)); +} + +#[test] +fn set_and_get_campaign_maintains_ttl() { + let env = Env::default(); + let (client, contract_id, admin, dispute_contract) = setup(&env); + client.initialize(&admin, &dispute_contract, &250); + + let business = Address::generate(&env); + let token = Address::generate(&env); + let campaign = Campaign { + id: 1, + business: business.clone(), + asset: ads_bazaar_shared::PayoutAsset { + token, + symbol: String::from_str(&env, "USDC"), + }, + total_budget: 1_000_000, + escrow_balance: 0, + max_creators: 5, + approved_count: 0, + status: ads_bazaar_shared::CampaignStatus::Draft, + application_deadline: env.ledger().timestamp() + 86_400, + completion_deadline: env.ledger().timestamp() + 604_800, + metadata_uri: String::from_str(&env, "ipfs://campaign"), + }; + + env.as_contract(&contract_id, || { + let _ = super::storage::set_campaign(&env, &campaign); + let loaded = super::storage::get_campaign(&env, 1).unwrap(); + assert_eq!(loaded.id, campaign.id); + assert_eq!(loaded.business, campaign.business); + }); +} + #[test] #[should_panic(expected = "not yet implemented")] fn create_campaign_is_not_yet_implemented() { @@ -68,8 +110,7 @@ fn create_campaign_is_not_yet_implemented() { // way) once `create_campaign` is implemented — replace this test with a // real assertion at that point. let env = Env::default(); - env.mock_all_auths(); - let (client, admin, dispute_contract) = setup(&env); + let (client, _contract_id, admin, dispute_contract) = setup(&env); client.initialize(&admin, &dispute_contract, &250); let business = Address::generate(&env); diff --git a/contracts/dispute-resolution/src/lib.rs b/contracts/dispute-resolution/src/lib.rs index 97d34a7..eb05d3c 100644 --- a/contracts/dispute-resolution/src/lib.rs +++ b/contracts/dispute-resolution/src/lib.rs @@ -37,6 +37,7 @@ impl DisputeResolutionContract { storage::set_admin(&env, &admin); storage::set_escrow_contract(&env, &escrow_contract); + storage::bump_instance(&env); Ok(()) } @@ -55,6 +56,7 @@ impl DisputeResolutionContract { creator: Address, reason_uri: String, ) -> Result { + storage::bump_instance(&env); raised_by.require_auth(); todo!("design + implement dispute raising — see doc comment above") } @@ -69,6 +71,7 @@ impl DisputeResolutionContract { dispute_id: DisputeId, arbiter: Address, ) -> Result<(), Error> { + storage::bump_instance(&env); admin.require_auth(); todo!("design + implement arbiter assignment — see doc comment above") } @@ -85,12 +88,14 @@ impl DisputeResolutionContract { dispute_id: DisputeId, outcome: DisputeOutcome, ) -> Result<(), Error> { + storage::bump_instance(&env); arbiter.require_auth(); todo!("design + implement dispute resolution — see doc comment above") } /// Read-only lookup of a dispute's current state. pub fn get_dispute(env: Env, dispute_id: DisputeId) -> Result { + storage::bump_instance(&env); storage::get_dispute(&env, dispute_id) } } diff --git a/contracts/dispute-resolution/src/storage.rs b/contracts/dispute-resolution/src/storage.rs index fb8ed71..fbd98eb 100644 --- a/contracts/dispute-resolution/src/storage.rs +++ b/contracts/dispute-resolution/src/storage.rs @@ -6,8 +6,8 @@ use soroban_sdk::{contracttype, Address, Env}; use crate::error::Error; use crate::types::Dispute; -const PERSISTENT_BUMP_LEDGERS: u32 = 518_400; -const PERSISTENT_LIFETIME_THRESHOLD: u32 = 500_000; +const LEDGER_BUMP: u32 = 535_680; +const LEDGER_THRESHOLD: u32 = 500_000; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -18,6 +18,12 @@ pub enum DataKey { Dispute(DisputeId), } +pub fn bump_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(LEDGER_THRESHOLD, LEDGER_BUMP); +} + pub fn is_initialized(env: &Env) -> bool { env.storage().instance().has(&DataKey::Admin) } @@ -59,10 +65,15 @@ pub fn next_dispute_id(env: &Env) -> DisputeId { } pub fn get_dispute(env: &Env, id: DisputeId) -> Result { + let key = DataKey::Dispute(id); + let dispute = env.storage() + .persistent() + .get(&key) + .ok_or(Error::DisputeNotFound)?; env.storage() .persistent() - .get(&DataKey::Dispute(id)) - .ok_or(Error::DisputeNotFound) + .extend_ttl(&key, LEDGER_THRESHOLD, LEDGER_BUMP); + Ok(dispute) } pub fn set_dispute(env: &Env, id: DisputeId, dispute: &Dispute) { @@ -70,7 +81,7 @@ pub fn set_dispute(env: &Env, id: DisputeId, dispute: &Dispute) { env.storage().persistent().set(&key, dispute); env.storage().persistent().extend_ttl( &key, - PERSISTENT_LIFETIME_THRESHOLD, - PERSISTENT_BUMP_LEDGERS, + LEDGER_THRESHOLD, + LEDGER_BUMP, ); }