Problem: Storing short-lived voting structures directly in persistent ledger storage scales up gas fees and leaves dead bytes after voting windows expire.
Solution: Move voting proposals to Soroban's Temporary storage bucket with automatic TTL-based cleanup.
This module provides the abstraction layer for managing temporary proposals:
pub fn store_temp_proposal<T: Contracttype>(
env: &Env,
key: &Symbol,
proposal: &T,
ttl: u32,
)- Stores proposal in temporary storage
- Automatically manages TTL lifecycle
- Called on proposal creation and vote updates
pub fn get_temp_proposal<T: Contracttype>(
env: &Env,
key: &Symbol,
) -> Option<T>- Retrieves proposal if it exists and hasn't expired
- Returns None if proposal is stale
pub fn remove_temp_proposal(env: &Env, key: &Symbol)- Explicitly removes proposal when executed
- Frees resources immediately vs waiting for TTL
| Operation | Before | After |
|---|---|---|
| Create | instance().set() |
store_temp_proposal() |
| Read | instance().get() |
get_temp_proposal() |
| Check exists | instance().has() |
has_temp_proposal() |
| Remove | instance().remove() |
remove_temp_proposal() |
| Check | Storage key: EMERGENCY_REVOCATION_KEY |
Storage key: EMERGENCY_REVOCATION_TEMP_KEY |
-
propose_emergency_revocation()- Now uses:
store_temp_proposal(env, &EMERGENCY_REVOCATION_TEMP_KEY, &proposal, DEFAULT_PROPOSAL_TTL) - TTL: ~10 days (172,800 ledgers)
- Now uses:
-
vote_emergency_revocation()- Reads:
get_temp_proposal(env, &EMERGENCY_REVOCATION_TEMP_KEY) - Updates:
store_temp_proposal()withEXTENDED_PROPOSAL_TTL - TTL: ~15 days (259,200 ledgers)
- Removes:
remove_temp_proposal()on execution
- Reads:
-
get_emergency_revocation_proposal()- Now:
get_temp_proposal(env, &EMERGENCY_REVOCATION_TEMP_KEY)
- Now:
pub fn purge_emergency_revocation_proposal(env: &Env) -> Result<(), ContractError>- Explicitly purges expired proposals from temporary storage
- Allows immediate reinitialization of new proposals
- Called via
purge_expired_revocation_proposal()contract function
pub fn has_active_emergency_revocation(env: &Env) -> bool- Check if proposal exists in temporary storage
- Returns false if expired or doesn't exist
- Called via
has_active_revocation_proposal()contract function
The main contract (src/lib.rs) exposes new functions:
pub fn purge_expired_revocation_proposal(env: Env) -> Result<(), ContractError>- Cleanup failed/stale proposals
- Can be called by any authorized party
- Security: relies on voting threshold model
pub fn has_active_revocation_proposal(env: Env) -> bool- Query if revocation proposal is currently active
- Useful for UI and governance monitoring
Remains unchanged for core contract state:
DATA_KEY: ContractData (admin, treasury)SIGNERS_KEY: Active multi-sig membersREVOKED_SIGNER_KEY: Revoked address listPENDING_OWNER_KEY: Ownership transfer nomineePAUSED_KEY: Contract pause flag
New location for short-lived voting structures:
EMERGENCY_REVOCATION_TEMP_KEY: EmergencyRevocationProposal (DEFAULT/EXTENDED_PROPOSAL_TTL)REVOCATION_TEMP_KEY: RevocationProposal (DEFAULT/EXTENDED_PROPOSAL_TTL)
1. Propose (admin/signer calls propose_emergency_revocation)
↓
→ Write to temp storage with DEFAULT_PROPOSAL_TTL (10 days)
→ Immediate execution if threshold met, else wait for votes
2. Vote (other signers call vote_emergency_revocation)
↓
→ Read from temp storage
→ Validate voter hasn't voted yet
→ Check if target is compromised key
→ Increment vote count
3a. Threshold Reached
↓
→ Revoke target from SIGNERS_KEY (persistent)
→ Update REVOKED_SIGNER_KEY (persistent)
→ Transfer admin if needed (persistent)
→ Remove from temp storage immediately
3b. Voting Window Expires (no manual action needed)
↓
→ After EXTENDED_PROPOSAL_TTL, Soroban network auto-purges
→ OR admin calls purge_expired_revocation_proposal()
3c. Voting Fails (explicit cleanup)
↓
→ Admin calls purge_expired_revocation_proposal()
→ Clears temp storage
→ Fresh proposal can be initiated
Located in src/temp_governance.rs:
pub const DEFAULT_PROPOSAL_TTL: u32 = 172_800; // ~10 days
pub const EXTENDED_PROPOSAL_TTL: u32 = 259_200; // ~15 days- Shorter TTL: If voting windows are typically < 1 week
- Longer TTL: If multi-sig requires > 15 days to coordinate
Note: Soroban network operates on 5-second block time. Formula: ledgers = seconds / 5
write operation cost: ~100-150 ops per proposal update
storage permanently occupied: ~500 bytes per proposal
write operation cost: ~20-30 ops per proposal update
storage auto-cleanup: 0 bytes after TTL
reduction: ~75% gas per operation, 100% storage recovery
- Old persistent keys (
EMERGENCY_REVOCATION_KEY,REVOCATION_KEY) retained - New code reads only from temporary storage
- If migration needed: Old proposals can be manually transferred
-
Proposal Lifecycle
- Test proposal creation in temp storage
- Verify vote updates extend TTL
- Confirm execution removes from temp
-
TTL Management
- Test proposal auto-purge after TTL (simulated)
- Verify explicit purge works correctly
- Confirm voting after purge creates new proposal
-
Edge Cases
- Double-voting prevention still works
- Compromised key can't vote on its own revocation
- Multiple concurrent proposals blocked
-
Query Functions
get_emergency_revocation()returns None after purgehas_active_revocation_proposal()correctly reflects state
For live contracts with existing proposals:
- Read old proposal from
EMERGENCY_REVOCATION_KEY - Write to temp storage with
EMERGENCY_REVOCATION_TEMP_KEY - Clear old persistent key
- Continue voting with temp storage
No consensus loss - voting can continue seamlessly.
purge_expired_revocation_proposal()events can be logged- Provides audit trail of cleanup actions
- Compare with network's auto-purge timeline
- Use
has_active_revocation_proposal()for governance dashboards - Monitor TTL remaining via proposal
proposed_attimestamp - Alert if proposal nearing expiration
- Soroban Temporary Storage: https://soroban.stellar.org/docs/learn/storing-data
- TTL and Storage: https://soroban.stellar.org/docs/learn/storing-data#temporary-storage
- Time Measurement: Block time = 5 seconds on Stellar