Skip to content

feat: prevent gov update from bricking rollup#22656

Open
just-mitch wants to merge 1 commit into
nextfrom
mitch/tmnt-521-rollup-gate-contract
Open

feat: prevent gov update from bricking rollup#22656
just-mitch wants to merge 1 commit into
nextfrom
mitch/tmnt-521-rollup-gate-contract

Conversation

@just-mitch
Copy link
Copy Markdown
Collaborator

@just-mitch just-mitch commented Apr 20, 2026

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): updateEscapeHatchsetEscapeHatch. Reverts on address(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. EscapeHatchUpdatedEscapeHatchSet. New errors ValidatorSelection__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 in specificRecipientBalance[recipient] (tracked in totalEarmarkedBalance). Reverts on address(0).
  • ASSET sent to the contract directly forms an unearmarked pool, implicitly available to whichever rollup is canonical at the time.
  • availableTo(recipient) = (balance - totalEarmarked) + specificRecipientBalance[recipient] for the canonical rollup; just specificRecipientBalance[recipient] otherwise.
  • claim(to, amount) no longer gates on msg.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 draw specificRecipientBalance[msg.sender]. Insufficient funds revert with RewardDistributor__InsufficientAvailable. Old (non-canonical) rollups can still drain anything earmarked to them across rotations.
  • recoverFrom(from, to, amount) (was recover(asset, to, amount)) mirrors claim's accounting under owner gating. The rename is deliberate: the new shape recover(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.
  • New errors: RewardDistributor__InsufficientAvailable, RewardDistributor__ZeroRollup, RewardDistributor__WrongRecoverMechanism.

Reward config — addresses immutable post-deployment: setRewardConfig no longer takes a full RewardConfig. It takes MutableRewardConfig, which exposes only sequencerBps and checkpointReward. The rewardDistributor and booster addresses are written exactly once in the constructor (RewardLib.initializeConfig) and immutable thereafter. Rotating either requires redeploying the rollup via Registry.addRollup. The post-deployment writer is RewardLib.updateConfig. RewardConfigUpdated event 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. FeeStore gains uint64 provingCostLastUpdate. New errors FeeLib__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: assertValidQueueConfig lifted into StakingLib and called from both the constructor and updateStakingQueueConfig. normalFlushSizeMin > 0 and normalFlushSizeQuotient > 0 for the life of the rollup; the path that could close deposits on a running rollup is gone.

Slasher swap — 60-day timelock: setSlasher removed, replaced by queueSetSlasher (owner) → cancelSetSlasher (owner) | finalizeSetSlasher (permissionless). SLASHER_EXECUTION_DELAY = 60 days exceeds 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 events PendingSlasherQueued, PendingSlasherCancelled. New errors Staking__NoPendingSlasher, Staking__SlasherNotReady. New views getPendingSlasher, getSlasherExecutionDelay.

setLocalEjectionThreshold — removed: mutator gone entirely; reader stays. Threshold is fixed at deploy.

updateManaTarget is deliberately left otherwise unchanged — its worst-case outcomes no longer mute the escape hatch.

Test plan

  • New unit + integration coverage:
    • 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 — new SlashLocalEjectionTest deploys 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.
  • Reward distributor: claim.t.sol, recover.t.sol, subsidizeAddress.t.sol (renamed from subsidizeRollup.t.sol) cover boundary, multi-recipient isolation, and canonical-rotation semantics. recover.t.sol exercises both recoverFrom (ASSET) and recoverWrongAsset (other tokens, including the named-error revert when called with ASSET). New invariant.t.sol adds 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.
  • Yarn-project: rollup_cheat_codes.ts gains clearProvingCostCooldown for tests that need to bump proving cost more than once; slash_veto_demo.test.ts deploys the slasher with the correct vetoer up front instead of swapping mid-test (now that setSlasher is gone).

@just-mitch just-mitch force-pushed the mitch/tmnt-521-rollup-gate-contract branch 5 times, most recently from 504903a to a019231 Compare April 20, 2026 19:21
@just-mitch just-mitch changed the title WIP: feat: prevent gov update from bricking rollup feat: prevent gov update from bricking rollup Apr 21, 2026
@just-mitch just-mitch added the ci-full Run all master checks. label Apr 21, 2026
@just-mitch just-mitch force-pushed the mitch/tmnt-521-rollup-gate-contract branch from a019231 to a45292e Compare April 21, 2026 00:25
@just-mitch just-mitch force-pushed the mitch/tmnt-521-rollup-gate-contract branch 2 times, most recently from 8c89fa4 to 6805039 Compare May 7, 2026 11:51
@just-mitch just-mitch force-pushed the mitch/tmnt-521-rollup-gate-contract branch from 6805039 to 0accbc6 Compare May 11, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-full Run all master checks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant