Repository navigation
Put revenue_pool upgrade behind a propose/execute/cancel timelock with real storage-layout validation #1402
Description
Summary
RevenuePool::upgrade replaces the contract's WASM immediately, in the same call the admin signs. There is no delay, no proposal visible on-chain, and no way to cancel. The storage-migration gate it runs passes zero_layout_hash as both the source and target layout, so it checks nothing beyond a non-zero hash and the version order. Vault and settlement both have a timelocked upgrade flow. The revenue pool, which holds pooled USDC on testnet, has none.
Background and evidence
- Immediate upgrade.
contracts/revenue_pool/src/lib.rs,upgrade(lines 869–912):caller.require_auth(), the emergency-pause check andrequire_admin, thenupdate_current_contract_wasm(new_wasm_hash)in the same call. - No real layout check.
let placeholder_layout = callora_storage_migration::zero_layout_hash(&env);is passed to bothvalidate_before_upgradeandfinalize_migration, andfinalize_migrationruns in the old code before the swap. - The docs promise more.
docs/UPGRADE.mddescribes timelocked upgrades, andcontracts/settlement/src/timelock.rshasUPGRADE_TIMELOCK_SECONDS = 172_800. - A pattern already exists in this crate. The pool already has a timelocked flow for a different action (
propose_emergency_drain,execute_emergency_drainandcancel_emergency_drain, lines 1019–1150). That flow is the pattern to follow.
Scope
In scope
- New entrypoints.
propose_upgrade(caller, new_wasm_hash): rejects a zero hash; storesPendingUpgrade { wasm_hash, proposed_at, execute_after, expires_at }; emitsupgrade_proposed.execute_upgrade(caller): requiresexecute_after <= now <= expires_at.cancel_upgrade(caller).get_pending_upgrade().
- Deprecate the old entrypoint.
upgradebecomes an alias forpropose_upgrade, as settlement does, so existing tooling does not upgrade instantly by accident. - Real layout validation.
- Replace the zero layout with
callora_storage_migration::layout_hash(&env, <schema descriptor>)for the current layout. - Record it with
record_deployed_schemaif it is absent. - Move
finalize_migrationto a post-swapmigrateentrypoint that the new code calls.
- Replace the zero layout with
- Pause policy. Execution is blocked while emergency-paused, but cancellation is still allowed.
- Docs. Update
docs/UPGRADE.mdanddocs/EVENT_SCHEMA.md.
Out of scope
- Changing
StorageMigrationValidatoritself (a separate issue). - Settlement and vault upgrade flows.
Design constraints
- Admin only. Only the admin may call these, with
require_authfirst. - Checked arithmetic for deadlines; overflow returns a typed error.
- Typed errors. No string panics in new code. Use
RevenuePoolErrorwith appended codes. - Size: the WASM size delta must be at most 3%. Report it.
Test plan
| # | Scenario | Expected |
|---|---|---|
| 1 | Propose then execute before execute_after |
timelock error |
| 2 | Execute inside the window with an uploaded wasm | upgraded; version stored; proposal cleared |
| 3 | Execute after expires_at |
expired error |
| 4 | Cancel, then execute | no pending error |
| 5 | Non-admin propose, execute or cancel | Unauthorized |
| 6 | Zero hash | rejected |
| 7 | Execute while emergency-paused | rejected; cancel still works |
| 8 | Legacy upgrade() call |
only proposes; the WASM is unchanged |
| 9 | Layout mismatch fixture | SchemaMismatch surfaced |
Regression proof. On main, upgrade() swaps the WASM in a single call. Row 8 asserts it no longer does.
Acceptance criteria
- All test plan rows pass (
cargo test -p callora-revenue-pool upgrade). -
cargo fmt --all -- --checkandcargo test --workspacepass, and CI is green. -
cargo build --release --target wasm32v1-none -p callora-revenue-poolsucceeds, and the size delta is reported. - Docs are updated, including the migration note for the deployed pool
CC5KINWRSLKSSQG4ZOTP3EZIOUVC6ER7HL5GZALYY4LVE6ZIMX4PMCSE. - Coverage of the new functions is at least 95%.
Getting started
git checkout -b feat/revenue-pool-upgrade-timelock
cargo test -p callora-revenue-poolModel the code on propose_emergency_drain / execute_emergency_drain in the same file and on contracts/settlement/src/timelock.rs.
PR requirements
- Link this issue (
Closes #<n>). - Explain the upgrade order for the deployed contract.
- Paste the test output and the size delta.
- Example commit:
feat(revenue-pool): timelocked propose/execute/cancel upgrade.
Priority rationale
High. A single admin signature can replace the code of a deployed pool that holds funds, with no warning. The fix spans governance, storage-migration validation and wasm upgrade testing.
Activity
abiolatousin-design commentedon Oct 18, 2026
@abiolatousin-design has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @abiolatousin-design to this issue.
drips-wave commentedon Oct 19, 2026
Congratulations, @abiolatousin-design! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @abiolatousin-design: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊
feat: Put revenue_pool upgrade behind a propose/execute/cancel tim (C…
feat: Put revenue_pool upgrade behind a propose/execute/cancel tim (C…