Skip to content

feat: time-locked contract upgrade governance - #25

Merged
Inkman007 merged 4 commits into
Ledger-Lenz:mainfrom
Manuel1234477:feat/upgrade-timelock
Jun 17, 2026
Merged

feat: time-locked contract upgrade governance#25
Inkman007 merged 4 commits into
Ledger-Lenz:mainfrom
Manuel1234477:feat/upgrade-timelock

Conversation

@Manuel1234477

@Manuel1234477 Manuel1234477 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a time-locked upgrade mechanism for the contract WASM. Soroban contracts can be upgraded by the admin via update_current_contract_wasm, which replaces the entire contract logic in one transaction. Without governance, a single admin key — or a compromised one — could silently install a backdoor or disable a security check with no warning. This gates every upgrade behind a mandatory on-chain delay during which anyone can inspect the pending proposal and react, and the admin can veto.

Flow

  1. propose_upgrade(new_wasm_hash) — admin commits to a WASM hash; stores an UpgradeProposal with executable_after = now + get_upgrade_delay(), emits upgrade_proposed. Code is unchanged.
  2. Monitoring window — ≥ MIN_UPGRADE_DELAY_SECS (48 h, configurable up to 14 d). Anyone can call get_pending_upgrade to inspect the committed hash and executable_after.
  3. execute_upgrade() — only after the delay; re-verifies now >= executable_after (never cached), then env.deployer().update_current_contract_wasm(...), clears the proposal, emits upgrade_executed.
    OR veto_upgrade() — cancels during the window (escape hatch for a malicious proposal / compromised key), emits upgrade_vetoed naming the caller.

Changes

  • constantsMIN_UPGRADE_DELAY_SECS (48 h), MAX_UPGRADE_DELAY_SECS (14 d), DEFAULT_UPGRADE_DELAY_SECS (48 h).
  • typesUpgradeProposal { new_wasm_hash, proposed_at, executable_after, proposed_by }; DataKey::PendingUpgrade, DataKey::UpgradeDelay.
  • errorsNoPendingUpgrade=20, UpgradeNotReady=21, UpgradeAlreadyPending=22, InvalidUpgradeDelay=23.
  • eventsupgrade_proposed, upgrade_executed, upgrade_vetoed.
  • lib — six functions (full rustdoc), all admin-gated; deadlines from env.ledger().timestamp().
  • storage — pending-upgrade and upgrade-delay helpers.
  • docs — README Upgrade Governance section with sequence diagram; SECURITY.md upgrade threat model + monitoring window.

Security considerations

  • Only the current admin can propose/execute/veto — a compromised service key has no upgrade powers.
  • Deadlines derive from env.ledger().timestamp() (deterministic, not caller-settable); execute_upgrade re-verifies the clock at execution time.
  • set_upgrade_delay is bounded to [MIN, MAX]; raising the delay is always safe, lowering it shortens the veto window and is documented as requiring community consensus. A lowered delay only affects future proposals.
  • UpgradeProposal.proposed_by + the upgrade_* events provide a full on-chain audit trail.

Tests (test_upgrade.rs)

All required cases — propose_upgrade_stores_proposal, execute_before_delay_rejected, execute_after_delay_succeeds, veto_clears_pending_upgrade, double_propose_rejected, upgrade_delay_below_min_rejected, upgrade_delay_above_max_rejected, get_pending_upgrade_no_proposal, execute_upgrade_clears_proposal — plus extras, using env.ledger().with_mut(|l| l.timestamp = ...) for time simulation.

Testing note

The pinned soroban-env-host 21.2.1 WASM validator rejects the reference-types feature that modern Rust (≥1.82) emits, so no buildable WASM can be uploaded in tests (and soroban-test-wasms isn't published to crates.io). The host explicitly permits a zero-byte upload in test builds (it is never instantiated) — the intended hook for exercising the upgrade primitive. The execute-path tests upload that hash; execute_upgrade_clears_proposal reads storage via env.as_contract(...) rather than re-invoking the swapped-out instance. This is purely a test mechanism — production calls the real update_current_contract_wasm.

Verification

cargo fmt --check clean · cargo clippy --all-targets -- -D warnings clean · cargo test --workspace = 65 passed · cargo build --target wasm32-unknown-unknown --release succeeds.

Closes #12

Gate WASM upgrades behind a mandatory on-chain time-lock so a single (or
compromised) admin key cannot silently replace the contract logic in one
transaction. Any observer can inspect a pending upgrade during the delay
window and the admin can veto it.

- constants: MIN/MAX/DEFAULT_UPGRADE_DELAY_SECS (48 h / 14 d / 48 h).
- types: UpgradeProposal { new_wasm_hash, proposed_at, executable_after,
  proposed_by }; DataKey::PendingUpgrade and DataKey::UpgradeDelay.
- errors: NoPendingUpgrade=20, UpgradeNotReady=21, UpgradeAlreadyPending=22,
  InvalidUpgradeDelay=23.
- events: upgrade_proposed / upgrade_executed / upgrade_vetoed.
- lib: propose_upgrade, execute_upgrade (re-verifies now >= executable_after
  at execution time, then update_current_contract_wasm), veto_upgrade,
  get_pending_upgrade, set_upgrade_delay (bounded), get_upgrade_delay. All
  admin-only; deadlines derive from env.ledger().timestamp().
- storage: pending-upgrade and upgrade-delay helpers.
- tests (test_upgrade.rs): all required cases plus extras, using
  env.ledger().with_mut(..) for time simulation.
- docs: README Upgrade Governance section with sequence diagram; SECURITY.md
  upgrade threat model and community monitoring window.
@Inkman007

Copy link
Copy Markdown
Contributor

three checks failing

# Conflicts:
#	README.md
#	contracts/ledgerlens-score/src/lib.rs
…/upgrade-timelock

# Conflicts:
#	README.md
#	contracts/ledgerlens-score/src/lib.rs
@Inkman007
Inkman007 merged commit 8d50030 into Ledger-Lenz:main Jun 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement time-locked contract upgrade mechanism with admin veto window

2 participants