Closes #433
Implements a permissionless storage garbage collection mechanism to clean up
expired governance proposals and completed/cancelled vesting schedules from
Soroban instance storage. Without cleanup, stale data accumulates indefinitely,
increasing state bloat and TTL extension costs. This PR adds two cleanup
functions — cleanup_proposal and cleanup_vesting_schedule — callable by
anyone after a 30-day grace period post-resolution or post-completion, with
appropriate event emissions for indexer reconciliation.
- Bug fix
- New feature
- Documentation
- Smart contract change
- Refactor
-
VoteProposal: Addedresolved_at: u32— set to the current ledger sequence byresolve_proposalandveto_proposal. Defaults to0for unresolved and pre-upgrade proposals (backward compatible). -
VestingSchedule: Addedcompleted_at: u32— set to the current ledger sequence bycancel_vestingandclaim_vested_installment(when the final installment is claimed). Defaults to0for active and pre-upgrade schedules (backward compatible).
GRACE_PERIOD_LEDGERS = 518_400(~30 days @ 5s/ledger). The post- resolution or post-completion window during which stale data is preserved for indexers.
-
cleanup_proposal(env, project_id)- Permissionless — anyone can call after
resolved_at + GRACE_PERIOD_LEDGERS. - Removes the
DataKey::Proposal, allDataKey::HasVotedentries (by iterating theVoterList), andDataKey::VoterList. - Panics if the proposal is not found, not resolved, or the grace period has not elapsed.
- Emits
prop_cleanevent. - Feature-gated behind
governance.
- Permissionless — anyone can call after
-
cleanup_vesting_schedule(env, donor, schedule_id)- Permissionless — anyone can call after
completed_at + GRACE_PERIOD_LEDGERS. - Removes the
DataKey::VestingScheduleentry. - Panics if the schedule is not found, still active (
completed_at == 0), or the grace period has not elapsed. - Emits
vest_cleanevent. - Feature-gated behind
vesting.
- Permissionless — anyone can call after
cancel_vestingno longer removes the schedule from storage immediately. Instead, it setscompleted_atto the current ledger sequence and persists the schedule. Unvested tokens are still returned to the donor atomically in the same transaction. The schedule is later removed bycleanup_vesting_scheduleafter the grace period elapses.
| Event Name | Topics | Data | Description |
|---|---|---|---|
prop_cln |
["prop_cln", project_id] |
() |
Proposal & vote data removed from storage |
vest_cln |
["vest_cln", donor, schedule_id] |
() |
Vesting schedule removed from storage |
- Added
vestingto the default feature set inCargo.tomlto ensure the vesting cleanup path is compiled and tested in the standard build.
| Test | Description |
|---|---|
test_cleanup_resolved_proposal |
Proposal cleaned up after resolution + grace period |
test_cleanup_unresolved_panics |
Cleaning unresolved proposal panics |
test_cleanup_before_grace_period_panics |
Cleaning too early panics |
test_cleanup_proposal_idempotent |
Second cleanup panics (proposal already removed) |
test_cleanup_vesting_completed |
Completed vesting schedule cleaned up |
test_cleanup_vesting_active_panics |
Active schedule cleanup panics |
test_cleanup_vesting_before_grace_period_panics |
Early cleanup panics |
test_cleanup_vesting_cancelled |
Cancelled schedule cleaned up after grace period |
# Format check
cargo fmt --all -- --check
# Clippy (no warnings)
cargo clippy --workspace -- -D warnings
# Unit tests (skip fuzz)
cargo test --features testutils --workspace -- --skip fuzz
# WASM slim build (size check)
cargo build --workspace --target wasm32v1-none --release --no-default-featuresNote: All CI checks have been verified locally and pass:
- ✅
cargo fmt --all -- --check— clean- ✅
cargo clippy --workspace -- -D warnings— clean- ✅
cargo test --features testutils --workspace -- --skip fuzz— 349 passed, 0 failed- ✅
cargo build --target wasm32v1-none --release --no-default-features— succeeds- ✅
wasm-opt -Oz→ 64,241 bytes (under 65,536 limit)
| File | Purpose |
|---|---|
contracts/indigopay-contract/src/lib.rs |
Struct fields, cleanup functions, events, tests |
contracts/indigopay-contract/Cargo.toml |
Added vesting to default features |
contracts/indigopay-contract/UPGRADE.md |
Backward compat docs, new fields, cleanup docs |
contracts/EVENTS.md |
Document prop_clean and vest_clean events |
All changes are backward compatible:
-
Struct fields are appended to
VoteProposalandVestingSchedule, not inserted. Pre-upgrade stored values deserialize withresolved_at: 0andcompleted_at: 0, which the cleanup functions correctly interpret as "eligible for cleanup immediately" (resolved but no timestamp) and "still active" (no completion timestamp) respectively. -
No
DataKeyvariants were added, removed, or reordered. The storage key discriminants are unchanged. -
cancel_vestingbehaviour change: Previously the schedule was removed from storage immediately. Now it persists withcompleted_atset, which means the schedule's storage entry survives until cleanup. This is a one-time transition — after the first cancellation under the new code, subsequent cancellations work the same way. The UPGRADE.md documents this change.
-
cleanup_proposalremoves proposal and all associated vote data after grace period -
cleanup_proposalpanics if proposal is not resolved -
cleanup_proposalpanics if grace period has not elapsed -
cleanup_vesting_scheduleremoves schedule data after grace period - Events emitted with cleaned item identifiers
- Cleanup reduces storage footprint (verifiable via
env.storage().instance().has()) - Tests pass — 349 passed, 0 failed (verified locally)
- WASM under 64 KB — 64,241 bytes after wasm-opt -Oz (verified locally)
- CI green — all checks pass (fmt, clippy, tests, WASM build + size)
-
Pre-upgrade fully-claimed vesting schedules cannot be cleaned up: Any
VestingSchedulethat had all installments claimed before the upgrade will havecompleted_at == 0andinstallments_released >= installment_count. Sinceclaim_vested_installmentpanics when already fully released andcleanup_vesting_schedulepanics whencompleted_at == 0, these schedules are permanently orphaned in storage. There is no on-chain donor index to enumerate them for a migration. This affects only pre-existing schedules that were fully claimed before the upgrade; all new schedules and partially-claimed ones will correctly receivecompleted_atupon their next claim or cancellation. -
Fuzz test (
prop_cleanup_only_removes_stale_data) not yet implemented: The issue specified this in testing requirements. It can be added in a follow-up PR infuzz_tests.rs.
Not applicable; this PR contains no UI changes.