This document outlines the storage optimization strategy for Nova Rewards smart contracts to minimize ledger entry fees on Stellar. Each data key is classified by storage type and includes TTL extension justification.
Used for contract metadata that rarely changes and has short lifetime.
Characteristics:
- Survives contract invocations
- Minimal cost
- No TTL extension needed
- Examples: admin address, contract configuration flags
Contracts using Instance Storage:
campaign: Admin address, pause stateredemption: Admin address, next redemption IDadmin_roles: Admin address, pending admin, signers, threshold
Used for critical state that must survive indefinitely.
Characteristics:
- Requires TTL extension to prevent expiry
- Higher cost per entry
- Must be extended in hot paths
- Examples: campaign data, user balances, redemption records
Contracts using Persistent Storage:
campaign: Campaign data, participants list, join statusredemption: Redemption requests, redemption rates, total redemptionsnova_token: User balances, allowancesreward_pool: Pool balance, daily limits, withdrawal history
Functions that frequently access persistent entries should extend TTL to amortize cost:
// Example: nova_token balance_of
fn balance_of(env: &Env, addr: &Address) -> i128 {
let key = DataKey::Balance(addr.clone());
let balance = env.storage().persistent().get(&key).unwrap_or(0i128);
// Extend TTL: 31 days (2,678,400 ledgers at 5s/ledger)
env.storage()
.persistent()
.extend_ttl(&key, 2_678_400, 2_678_400);
balance
}- Short-lived data (temporary redemptions): 7 days (604,800 ledgers)
- Standard data (balances, campaigns): 31 days (2,678,400 ledgers)
- Critical data (admin records): 365 days (31,536,000 ledgers)
- Admin address → Instance storage (no TTL needed)
- Pause state → Instance storage (no TTL needed)
- Campaign data → Persistent storage (extend on read/write)
- Participants list → Persistent storage (extend on modification)
- Join status → Persistent storage (no extension needed - write-once)
Cost Optimization:
- Campaign data accessed in
set_active(),join_campaign(),distribute_reward()→ extend TTL - Participants list accessed in
join_campaign()→ extend TTL - Join status is write-once, no extension needed
- Admin address → Instance storage (no TTL needed)
- Next ID counter → Instance storage (no TTL needed)
- Redemption requests → Persistent storage (extend on access)
- Redemption rates → Instance storage (rarely changes)
- Total redemptions → Instance storage (accumulator, no TTL needed)
Cost Optimization:
- Redemption rates stored in instance (not persistent) to reduce costs
- Total redemptions stored in instance (accumulator pattern)
- Redemption requests extended on
get()andconfirm()
- Admin address → Instance storage (no TTL needed)
- User balances → Persistent storage (extend on every access)
- Allowances → Persistent storage (extend on access)
Cost Optimization:
- Balances extended in
balance_of()helper (called in hot paths) - Allowances extended in
allowance_of()helper - TTL: 31 days for standard operations
- Admin address → Instance storage (no TTL needed)
- Nova token address → Instance storage (no TTL needed)
- Lock state → Instance storage (no TTL needed)
- Daily limits → Instance storage (configuration)
- Withdrawal history → Persistent storage (extend on access)
Cost Optimization:
- Configuration stored in instance (no TTL)
- Withdrawal history extended on
withdraw()calls
- Campaign contract: ~500 bytes per campaign (persistent)
- Redemption contract: ~200 bytes per request (persistent)
- Nova token: ~100 bytes per balance entry (persistent)
- Campaign contract: ~500 bytes per campaign (no change, but TTL extended efficiently)
- Redemption contract: ~50 bytes per request (moved rates to instance)
- Nova token: ~100 bytes per balance (no change, but TTL extended in hot paths)
Estimated Savings:
- 30-40% reduction in TTL extension costs through strategic instance storage usage
- Amortized cost reduction through hot-path extensions
- Data that rarely changes (admin, configuration)
- Data with short lifetime (temporary flags)
- Accumulators that don't need historical records
- Metadata about the contract itself
- User-specific data (balances, campaign participation)
- Historical records (redemption requests)
- Data that must survive contract upgrades
- Data accessed across multiple transactions
- Extend TTL in functions that read/write the entry
- Use consistent TTL values (31 days for standard data)
- Extend in hot paths to amortize cost
- Document TTL strategy in code comments
-
Temporary Storage: Use for short-lived data (< 1 hour)
- Pending transactions
- Rate-limiting counters
- Temporary state during multi-step operations
-
Batch Operations: Combine multiple TTL extensions
- Reduce number of storage operations
- Batch updates in single transaction
-
Lazy TTL Extension: Only extend when approaching expiry
- Monitor TTL in read operations
- Extend only if TTL < 7 days remaining