fix(proxy): bound the upgrade delay and announce every change - #1234
Merged
nanaf6203-bit merged 1 commit intoSep 26, 2026
Merged
nanaf6203-bit merged 1 commit into
nanaf6203-bit merged 1 commit into
Conversation
`set_upgrade_delay_blocks` checked nothing but the admin. The delay is
the entire safety model of the two-step upgrade, so an unbounded setter
means a compromised admin can remove the timelock outright.
`0` is the sharp end: `set_implementation` computes
`upgrade_effective_at = current_block + delay`, and
`confirm_implementation` only requires `current_block >=
upgrade_effective_at`. With a delay of 0 the two fit in one block, so
stage-then-confirm becomes a single transaction and no observer gets a
window in which to notice. The delay is now constrained to
[MIN_UPGRADE_DELAY_BLOCKS, MAX_UPGRADE_DELAY_BLOCKS].
The cap is not only about "unusably long" delays. A delay anywhere near
`u64::MAX` overflows that same addition, and the wrapped result lands in
the past -- so a huge delay was never a safe way to freeze upgrades; it
silently removed the timelock just as effectively as 0. `set_implementation`
now uses `saturating_add` as well, so a saturated sum leaves the upgrade
unconfirmable rather than instantly confirmable, which is the safe
direction to fail in.
Every accepted change emits `UpgradeDelayChanged { old_delay, new_delay,
by }`. Previously a delay change left no trace: the only way to observe
one was to diff storage around the transaction, so shortening the delay
ahead of an upgrade was invisible until the upgrade landed.
`InvalidUpgradeDelay` is appended to `ProxyError` so no existing
discriminant moves, and the authorisation check still runs first so a
non-admin cannot probe the bounds. A pending upgrade's `effective_at` is
computed at stage time and is deliberately left alone, so shortening the
delay cannot pull an already-staged upgrade forward.
The three constants live in this contract rather than in
`propchain-traits`, contrary to the issue's suggestion: the crate
deliberately has no `propchain-traits` dependency, and a proxy is the
last place that should take on one.
Closes MettaChain#1170
Closes MettaChain#1167
Closes MettaChain#1168
Closes MettaChain#1169
|
@kilodesodiq-arch Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
4 tasks
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
Bounds the proxy's upgrade delay and emits an event on every change.
Closes #1170
Closes #1167
Closes #1168
Closes #1169
Why
set_upgrade_delay_blocksvalidated nothing but the admin. The delay is the safety model of the two-step upgrade, so an unbounded setter means a compromised or rogue admin can remove the timelock outright.0is the sharp end.set_implementationcomputes:and
confirm_implementationonly requirescurrent_block >= upgrade_effective_at. With a delay of0both fit inside one block, so stage-then-confirm collapses into a single transaction and no observer gets a window in which to notice.The overflow nobody was looking for
The issue frames the upper bound as "a huge value can brick future upgrades". Worth being precise about, because the actual behaviour is worse than that: a huge delay was never a safe way to freeze upgrades — it silently removed the timelock.
current_block + u64::MAXoverflows. The wrapped result lands in the past,confirm_implementation'scurrent_block < upgrade_effective_atcheck passes immediately, and the "delayed" upgrade is confirmable at once. Sou64::MAXand0fail the same way, from opposite ends: both make the upgrade instantly confirmable. (Under a debug build the same addition panics instead.)This is the concrete reason for the cap, and the reason
set_implementationnow usessaturating_addas well. A saturated sum leavesupgrade_effective_atatu64::MAX, so the upgrade stays unconfirmable — the safe direction to fail in, and the opposite of what wrapping does.What changed
contracts/proxy/src/lib.rsDEFAULT_UPGRADE_DELAY_BLOCKS = 100— identical to the literal the constructor hard-coded before, so a freshly deployed proxy behaves exactly as it did.MIN_UPGRADE_DELAY_BLOCKS = 10— ~1 minute. The floor below which the delay stops being a review window.MAX_UPGRADE_DELAY_BLOCKS = 432_000— 30 days, matchingLOCK_PERIOD_30_DAYSinpropchain-traits.ProxyError::InvalidUpgradeDelay, appended last so no existing discriminant moves.UpgradeDelayChanged { old_delay, new_delay, by }, withold_delayandnew_delayas indexed topics andbyin the payload.set_upgrade_delay_blocksnow rejects anything outside the inclusive range, and emits the event on success only.set_implementationusessaturating_add.upgrade_delay_bounds()so a UI does not have to hard-code the numbers to validate a proposed value.Ordering and scope
ensure_admin()still runs first, so a non-admin cannot use the error to distinguish "out of range" from "out of range, but only for admins". There is a test pinning that.effective_atis computed at stage time and is deliberately left alone, so shortening the delay cannot pull an already-staged upgrade forward. Pinned by a test.Tests
16 new tests, all in the existing inline module:
0is rejected; every value in1..MINis rejected;MINandMAXare accepted;MAX + 1,1_000_000andu64::MAXare rejected.u64::MAXis rejected, and after staging,upgrade_effective_atis still in the future.MAXgives exactlycurrent + MAXwith no wrap.changing_the_delay_emits_an_eventasserts one event, that both the old and new value appear among the topics, and that the payload decodes to the admin.default → MINand thenMIN → 500, so an observer can reconstruct the sequence.effective_at.The pre-existing
set_delay_works(50) still holds, since 50 is inside the new range.Note on where the constants live
The issue asks for constants "matching
PROXY_*constants from traits". Those constants do not exist — there is noPROXY_*inpropchain-traits, and there was never aPROXY_*prefix in that file. I put them in the proxy crate instead, for two reasons:propchain-proxydeliberately has nopropchain-traitsdependency. Adding one is aCargo.tomlchange plus a hand-editedCargo.lock, which is not something to do blind in a PR that is not about dependencies.If the intent is to centralise them in
propchain-traitsalongside the other*_BLOCKSconstants, that is a reasonable follow-up and a one-line move once the dependency is added deliberately.Integration changes
ProxyErrorgains one variant, appended last.ProxyErroris defined in, and only used by, this crate — nothing in the workspace matches on it exhaustively.Cargo.lockchange.upgrade_delay_blocksbehaviour for values already inside the range is unchanged.Test plan
cargo test -p propchain-proxy— not run. No code validation was performed, by explicit instruction; this change is source-only and manually reviewed.cargo fmt --all -- --check— not run, same reason.cargo clippy --workspace --all-targets -- -D warnings— not run, same reason.cargo build --workspace— not run, same reason.The 16 tests added here are expected to compile and pass, but they are unverified.
The one assertion I would check first on a machine with a toolchain is
changing_the_delay_emits_an_event. It compares recorded topics againstEncodeof the expectedu64, which is the standard ink! idiom and how I believe ink! encodes a numeric topic, but I could not confirm it against the macro expansion. If ink! pads or transforms numeric topics, that test — and only that test — will need its comparison adjusted; the implementation is unaffected.Env vars