feat: time-locked contract upgrade governance - #25
Merged
Conversation
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.
Contributor
|
three checks failing |
# Conflicts: # README.md # contracts/ledgerlens-score/src/lib.rs
…/upgrade-timelock # Conflicts: # README.md # contracts/ledgerlens-score/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
propose_upgrade(new_wasm_hash)— admin commits to a WASM hash; stores anUpgradeProposalwithexecutable_after = now + get_upgrade_delay(), emitsupgrade_proposed. Code is unchanged.MIN_UPGRADE_DELAY_SECS(48 h, configurable up to 14 d). Anyone can callget_pending_upgradeto inspect the committed hash andexecutable_after.execute_upgrade()— only after the delay; re-verifiesnow >= executable_after(never cached), thenenv.deployer().update_current_contract_wasm(...), clears the proposal, emitsupgrade_executed.OR
veto_upgrade()— cancels during the window (escape hatch for a malicious proposal / compromised key), emitsupgrade_vetoednaming the caller.Changes
MIN_UPGRADE_DELAY_SECS(48 h),MAX_UPGRADE_DELAY_SECS(14 d),DEFAULT_UPGRADE_DELAY_SECS(48 h).UpgradeProposal { new_wasm_hash, proposed_at, executable_after, proposed_by };DataKey::PendingUpgrade,DataKey::UpgradeDelay.NoPendingUpgrade=20,UpgradeNotReady=21,UpgradeAlreadyPending=22,InvalidUpgradeDelay=23.upgrade_proposed,upgrade_executed,upgrade_vetoed.env.ledger().timestamp().Upgrade Governancesection with sequence diagram; SECURITY.md upgrade threat model + monitoring window.Security considerations
env.ledger().timestamp()(deterministic, not caller-settable);execute_upgradere-verifies the clock at execution time.set_upgrade_delayis 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+ theupgrade_*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, usingenv.ledger().with_mut(|l| l.timestamp = ...)for time simulation.Testing note
The pinned
soroban-env-host21.2.1 WASM validator rejects thereference-typesfeature that modern Rust (≥1.82) emits, so no buildable WASM can be uploaded in tests (andsoroban-test-wasmsisn'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_proposalreads storage viaenv.as_contract(...)rather than re-invoking the swapped-out instance. This is purely a test mechanism — production calls the realupdate_current_contract_wasm.Verification
cargo fmt --checkclean ·cargo clippy --all-targets -- -D warningsclean ·cargo test --workspace= 65 passed ·cargo build --target wasm32-unknown-unknown --releasesucceeds.Closes #12