This document describes the storage layout optimizations applied to Nova Rewards smart contracts to stay within Soroban's WASM size and storage budget limits.
This audit reviewed the nova-rewards, nova_token, vesting, referral, reward_pool, and admin_roles contracts. Existing keys are already mostly consolidated; the new DailyUsage struct in reward_pool further reduces per-wallet storage overhead.
Where multiple fields are always read/written together, they have been consolidated into single struct values to reduce storage overhead.
Using Bytes or BytesN instead of String for fixed-length identifiers reduces encoding overhead.
All persistent storage entries have appropriate TTL extensions to prevent ledger expiry.
Storage Keys:
Admin(instance) - Administrator addressBalance(Address)(instance) - User point balancesMigratedVersion(instance) - Contract version for migrationsXlmToken(instance) - XLM SAC token contract addressRouter(instance) - DEX router contract address
Optimizations Applied:
- All configuration fields use instance storage (no TTL management needed)
- Balance lookups use composite keys
Balance(Address)instead of separate mapping - Fixed-point arithmetic uses
i128to prevent overflow without additional storage
WASM Size: Measured with stellar contract inspect --wasm
Storage Keys:
Admin(instance) - Administrator addressBalance(Address)(persistent) - Token balances per addressAllowance(Address, Address)(persistent) - Spending allowances
Optimizations Applied:
- Composite keys for balances and allowances eliminate need for nested maps
- Persistent storage used only for user data that must survive contract upgrades
- Instance storage for admin (immutable after init)
TTL Extensions:
- Balance and Allowance entries: Extended on every read/write operation
- Recommended TTL: 31 days (2,678,400 ledgers)
Storage Keys:
Admin(instance) - Administrator addressPoolBalance(instance) - Total pool balanceSchedule(Address, u32)(persistent) - Vesting schedules by beneficiary and IDNextId(Address)(instance) - Counter for schedule IDs per beneficiary
Optimizations Applied:
VestingSchedulestruct consolidates all schedule fields (beneficiary, amounts, times, released)- Composite key
Schedule(Address, u32)enables efficient per-user schedule lookups - Pool balance in instance storage (single global value)
TTL Extensions:
- Schedule entries: Extended when created and on each release
- Recommended TTL: 365 days (31,536,000 ledgers) for long-term vesting
Storage Keys:
Admins(instance) - Set of admin addressesMultisigThreshold(instance) - Required signature countPendingAdmin(instance) - Two-step transfer state
Optimizations Applied:
- All admin data in instance storage (configuration-level data)
- No persistent storage needed (admin changes are rare)
Storage Keys:
Admin(instance) - Administrator addressUserReferrer(Address)(persistent) - Referrer for each userReferralCount(Address)(persistent) - Count of referrals per userRegistrationCounter(instance) - Global registration counter
Optimizations Applied:
- Composite keys for user-specific data
- Global counter in instance storage
- Persistent storage only for user relationships
TTL Extensions:
- UserReferrer and ReferralCount: Extended on registration and queries
- Recommended TTL: 90 days (7,776,000 ledgers)
Storage Keys:
Admin(instance) - Administrator addressBalance(instance) - Pool balanceDailyLimit(instance) - Global per-wallet daily withdraw limitDailyUsage(Address)(persistent) - Per-wallet daily usage and window start
Optimizations Applied:
- Single balance field instead of multiple pool tracking fields
- Daily usage consolidated into a single
DailyUsagestruct for each wallet - Daily limit stored in instance storage for admin configuration
TTL Extensions:
DailyUsageentries: Extended on each withdraw/check to preserve rolling window state- Recommended TTL: 2 days (172,800 ledgers)
- Used for: Configuration, admin addresses, global counters
- Lifetime: Tied to contract instance
- No TTL management required
- Limit: ~10KB per contract instance
- Used for: User balances, schedules, relationships
- Lifetime: Managed via TTL extensions
- Must call
extend_ttl()on read/write - Limit: ~100KB per entry
- Not currently used in Nova Rewards contracts
- Lifetime: Single ledger
- Use case: Intermediate computation results
// Example: Extending TTL on persistent storage read
let balance: i128 = env
.storage()
.persistent()
.get(&DataKey::Balance(addr.clone()))
.unwrap_or(0);
// Extend TTL by 31 days (2,678,400 ledgers at 5s/ledger)
env.storage()
.persistent()
.extend_ttl(&DataKey::Balance(addr.clone()), 2_678_400, 2_678_400);Run before and after optimization:
cd contracts
stellar contract build
stellar contract inspect --wasm target/wasm32v1-none/release/nova_rewards.wasm
stellar contract inspect --wasm target/wasm32v1-none/release/nova_token.wasm
stellar contract inspect --wasm target/wasm32v1-none/release/vesting.wasm- Maximum WASM size: 256 KB (Soroban limit)
- Target size: < 128 KB (50% headroom for future features)
- Current sizes: (to be measured)
- Pagination: For contracts with unbounded lists, implement pagination to avoid loading entire collections
- Lazy Loading: Load struct fields on-demand rather than entire structs
- Compression: Use bit-packing for boolean flags and small enums
- Archive Old Data: Move historical data to off-chain storage, keep only recent entries on-chain