Skip to content

fix(contract): replace single-step upgrade with two-step timelock (is… - #1180

Merged
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
CodedTricks:fix/issue-6-contract-upgrade-timelock
Aug 28, 2026
Merged

fix(contract): replace single-step upgrade with two-step timelock (is…#1180
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
CodedTricks:fix/issue-6-contract-upgrade-timelock

Conversation

@CodedTricks

@CodedTricks CodedTricks commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The previous upgrade() entrypoint performed an immediate WASM swap on a single admin signature with no on-chain event, giving monitoring systems, multisig quorums, and the community zero warning before attacker-controlled code could go live. SECURITY.md classifies a malicious WASM swap as the single Critical-severity threat scenario.

This commit resolves the issue with a mandatory two-step upgrade flow and a ~28.8-hour timelock (UPGRADE_TIMELOCK_LEDGERS = 17,280 ledgers):

Contract changes (contracts/token-factory/src/lib.rs):

  • Add UPGRADE_TIMELOCK_LEDGERS constant (17,280 ledgers ≈ 28.8 hours).
  • Add pending_upgrade_hash: Option<BytesN<32>> and pending_upgrade_ready_at: Option to FactoryState.
  • Remove single-step upgrade(). Replace with: propose_upgrade(admin, new_wasm_hash) — records the hash and a ready_at ledger; emits upg_prop event immediately. execute_upgrade(admin, new_wasm_hash) — performs the WASM swap only after the timelock has elapsed and the hash matches exactly; emits upg_exec. Fails with UpgradeNotReady (28), NoUpgradePending (29), or UpgradeHashMismatch (30). cancel_upgrade(admin) — aborts a pending proposal at any time; emits upg_can. Idempotent.
  • Add Error variants: UpgradeNotReady=28, NoUpgradePending=29, UpgradeHashMismatch=30.
  • Bump CURRENT_SCHEMA_VERSION to 5.
  • Add v5 migration step: seeds pending_upgrade_hash/ready_at to None on existing deployments; idempotent.

Tests (contracts/token-factory/src/test.rs):

  • Replace test_upgrade_unauthorized with comprehensive timelock suite:
    • execute_upgrade fails before timelock elapses (UpgradeNotReady).
    • execute_upgrade fails with wrong hash (UpgradeHashMismatch).
    • execute_upgrade fails with no proposal (NoUpgradePending).
    • cancel_upgrade clears pending proposal; subsequent execute fails.
    • cancel_upgrade is idempotent when nothing pending.
    • propose_upgrade emits upg_prop event.
    • cancel_upgrade emits upg_can event.
    • propose_upgrade stores correct hash and ready_at in state.
    • Second proposal overwrites first and resets timelock.
    • Unauthorized callers rejected on all three new entrypoints.
  • Add schema v5 migration tests (v4→v5, idempotent at v5, full v0→v5 walk in one call).

Docs updated:

  • docs/incident-response.md: describe the timelock as an active defense; replace polling-only detection with event-based monitoring for upg_prop/ upg_exec/upg_can; update threat model table; split detection section into primary (events) and defense-in-depth (hash polling).
  • docs/contract-abi.md: document propose_upgrade/execute_upgrade/ cancel_upgrade; add error codes 28–30; add upg_prop/upg_exec/upg_can to events table; add schema v5 to migrate() description.
  • docs/mainnet-deployment-checklist.md: add step-by-step two-step upgrade procedure and upgrade event monitoring to incident readiness.
  • README.md: update schema version table with v5; replace upgrade procedure with two-step propose/wait/execute flow including cancel.

Closes #1094

…sue Favourorg#6)

The previous upgrade() entrypoint performed an immediate WASM swap on a
single admin signature with no on-chain event, giving monitoring systems,
multisig quorums, and the community zero warning before attacker-controlled
code could go live. SECURITY.md classifies a malicious WASM swap as the
single Critical-severity threat scenario.

This commit resolves the issue with a mandatory two-step upgrade flow and
a ~28.8-hour timelock (UPGRADE_TIMELOCK_LEDGERS = 17,280 ledgers):

Contract changes (contracts/token-factory/src/lib.rs):
- Add UPGRADE_TIMELOCK_LEDGERS constant (17,280 ledgers ≈ 28.8 hours).
- Add pending_upgrade_hash: Option<BytesN<32>> and
  pending_upgrade_ready_at: Option<u64> to FactoryState.
- Remove single-step upgrade(). Replace with:
    propose_upgrade(admin, new_wasm_hash) — records the hash and a
      ready_at ledger; emits upg_prop event immediately.
    execute_upgrade(admin, new_wasm_hash) — performs the WASM swap only
      after the timelock has elapsed and the hash matches exactly;
      emits upg_exec. Fails with UpgradeNotReady (28),
      NoUpgradePending (29), or UpgradeHashMismatch (30).
    cancel_upgrade(admin) — aborts a pending proposal at any time;
      emits upg_can. Idempotent.
- Add Error variants: UpgradeNotReady=28, NoUpgradePending=29,
  UpgradeHashMismatch=30.
- Bump CURRENT_SCHEMA_VERSION to 5.
- Add v5 migration step: seeds pending_upgrade_hash/ready_at to None
  on existing deployments; idempotent.

Tests (contracts/token-factory/src/test.rs):
- Replace test_upgrade_unauthorized with comprehensive timelock suite:
  - execute_upgrade fails before timelock elapses (UpgradeNotReady).
  - execute_upgrade fails with wrong hash (UpgradeHashMismatch).
  - execute_upgrade fails with no proposal (NoUpgradePending).
  - cancel_upgrade clears pending proposal; subsequent execute fails.
  - cancel_upgrade is idempotent when nothing pending.
  - propose_upgrade emits upg_prop event.
  - cancel_upgrade emits upg_can event.
  - propose_upgrade stores correct hash and ready_at in state.
  - Second proposal overwrites first and resets timelock.
  - Unauthorized callers rejected on all three new entrypoints.
- Add schema v5 migration tests (v4→v5, idempotent at v5,
  full v0→v5 walk in one call).

Docs updated:
- docs/incident-response.md: describe the timelock as an active defense;
  replace polling-only detection with event-based monitoring for upg_prop/
  upg_exec/upg_can; update threat model table; split detection section
  into primary (events) and defense-in-depth (hash polling).
- docs/contract-abi.md: document propose_upgrade/execute_upgrade/
  cancel_upgrade; add error codes 28–30; add upg_prop/upg_exec/upg_can
  to events table; add schema v5 to migrate() description.
- docs/mainnet-deployment-checklist.md: add step-by-step two-step
  upgrade procedure and upgrade event monitoring to incident readiness.
- README.md: update schema version table with v5; replace upgrade
  procedure with two-step propose/wait/execute flow including cancel.

Closes issue Favourorg#6 from the 2026-08 codebase audit (ISSUES.md).
@Ejirowebfi
Ejirowebfi merged commit cb9f98a into Favourorg:main Aug 28, 2026
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
## [1.8.7](v1.8.6...v1.8.7) (2026-08-28)

### Bug Fixes

* **auth:** replace timing-unsafe JWT signature comparison with timingSafeEqual ([#1179](#1179)) ([8925440](8925440)), closes [#4](#4)
* **contract:** replace single-step upgrade with two-step timelock (issue [#6](#6)) ([#1180](#1180)) ([cb9f98a](cb9f98a))
* split rate-limit buckets, guard cross-tab network change, enforce error discriminant uniqueness ([#1178](#1178)) ([7ddf2c4](7ddf2c4)), closes [#1162](#1162) [#1163](#1163) [#1164](#1164) [#1162](#1162) [#1163](#1163) [#1164](#1164)
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.8.7 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🟠 Contract upgrade has no on-chain event and no timelock

2 participants