feat: prevent gov update from bricking rollup#22656
Open
just-mitch wants to merge 1 commit into
Open
Conversation
504903a to
a019231
Compare
a019231 to
a45292e
Compare
8c89fa4 to
6805039
Compare
6805039 to
0accbc6
Compare
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.
Implements AZIP-2.
The shared theme: governance and rollup-config setters that could mute or strand the escape hatch — or strand validators or rewards — are removed, made one-shot, delay-gated, or rate-limited.
Escape hatch (one-shot):
updateEscapeHatch→setEscapeHatch. Reverts onaddress(0)and on any second call. Once a rollup has an escape hatch, it cannot be replaced or removed; rollups that want no escape hatch simply never call the setter.EscapeHatchUpdated→EscapeHatchSet. New errorsValidatorSelection__EscapeHatchAlreadySet,ValidatorSelection__EscapeHatchCannotBeZero.Reward distributor — per-address earmarking + canonical inheritance: Anyone can subsidize block production on a specific address (typically a rollup instance), regardless of whether it is canonical. Rollups are not privileged at the bookkeeping layer; the only place "rollup" enters is that the canonical rollup is the sole address with access to the implicit (un-earmarked) pool.
subsidizeAddress(recipient, amount)is permissionless and earmarks ASSET to a specific recipient inspecificRecipientBalance[recipient](tracked intotalEarmarkedBalance). Reverts onaddress(0).availableTo(recipient)=(balance - totalEarmarked) + specificRecipientBalance[recipient]for the canonical rollup; justspecificRecipientBalance[recipient]otherwise.claim(to, amount)no longer gates onmsg.sender == canonicalRollup(). Authorization is implicit through accounting: canonical callers draw the implicit pool first and fall through to their earmarked balance; any other caller can only drawspecificRecipientBalance[msg.sender]. Insufficient funds revert withRewardDistributor__InsufficientAvailable. Old (non-canonical) rollups can still drain anything earmarked to them across rotations.recoverFrom(from, to, amount)(wasrecover(asset, to, amount)) mirrorsclaim's accounting under owner gating. The rename is deliberate: the new shaperecover(address,address,uint256)would have collided on its 4-byte selector with the old asset-recovery shape, silently re-interpreting governance calldata that referenced the previous ABI. Renaming forces a clean break.recoverWrongAsset(asset, to, amount)is the owner-only path for non-ASSET tokens.RewardDistributor__InsufficientAvailable,RewardDistributor__ZeroRollup,RewardDistributor__WrongRecoverMechanism.Reward config — addresses immutable post-deployment:
setRewardConfigno longer takes a fullRewardConfig. It takesMutableRewardConfig, which exposes onlysequencerBpsandcheckpointReward. TherewardDistributorandboosteraddresses are written exactly once in the constructor (RewardLib.initializeConfig) and immutable thereafter. Rotating either requires redeploying the rollup viaRegistry.addRollup. The post-deployment writer isRewardLib.updateConfig.RewardConfigUpdatedevent signature follows.setProvingCostPerMana— rate-limited: floor of 2 (MIN_PROVING_COST_PER_MANA), 30-day cooldown (first post-init update waived), symmetric 3/2 multiplicative step.FeeStoregainsuint64 provingCostLastUpdate. New errorsFeeLib__ProvingCostBelowFloor,FeeLib__ProvingCostCooldown,FeeLib__ProvingCostStepExceeded. With 3/2 per 30 days, the value needs ~170 days to move 10× and ~340 days to move 100×.Staking queue invariants — enforced on every write:
assertValidQueueConfiglifted intoStakingLiband called from both the constructor andupdateStakingQueueConfig.normalFlushSizeMin > 0andnormalFlushSizeQuotient > 0for the life of the rollup; the path that could close deposits on a running rollup is gone.Slasher swap — 60-day timelock:
setSlasherremoved, replaced byqueueSetSlasher(owner) →cancelSetSlasher(owner) |finalizeSetSlasher(permissionless).SLASHER_EXECUTION_DELAY = 60 daysexceeds the ~38-day withdrawal window so validators who object can exit before the change lands. Queueing while a change is pending overwrites it and resets the timer. New eventsPendingSlasherQueued,PendingSlasherCancelled. New errorsStaking__NoPendingSlasher,Staking__SlasherNotReady. New viewsgetPendingSlasher,getSlasherExecutionDelay.setLocalEjectionThreshold— removed: mutator gone entirely; reader stays. Threshold is fixed at deploy.updateManaTargetis deliberately left otherwise unchanged — its worst-case outcomes no longer mute the escape hatch.Test plan
setEscapeHatchOneShot.t.sol— every transition of the one-shot guard, including zero-then-nonzero.ProvingCostRateLimit.t.sol— floor, step boundaries (up and down), cooldown boundaries, and a 10-step amplification check that bounds (3/2)^10 growth.setSlasher.t.sol— queue/cancel/finalize states, finalize permissionless, overwrite-pending semantics.setLocalEjectionThresholdRemoval.t.sol— selector unreachable post-removal.slash.t.sol— newSlashLocalEjectionTestdeploys with a non-zero threshold to exercise the local-ejection path.updateStakingQueueConfig.t.sol— invalid-config reverts.initialize.t.sol— proving-cost floor enforced at construction.claim.t.sol,recover.t.sol,subsidizeAddress.t.sol(renamed fromsubsidizeRollup.t.sol) cover boundary, multi-recipient isolation, and canonical-rotation semantics.recover.t.solexercises bothrecoverFrom(ASSET) andrecoverWrongAsset(other tokens, including the named-error revert when called with ASSET). Newinvariant.t.soladds a stateful fuzz handler asserting three accounting identities (balance ≥ totalEarmarked, sum of specifics == totalEarmarked, canonical availableTo identity) under random subsidize/claim/recover/donate/rotate sequences.rollup_cheat_codes.tsgainsclearProvingCostCooldownfor tests that need to bump proving cost more than once;slash_veto_demo.test.tsdeploys the slasher with the correct vetoer up front instead of swapping mid-test (now thatsetSlasheris gone).