Skip to content

Split failed proposal 24 into proposal 24 (de-whitelist + guard) and proposal 25 (un-nominate), under the EIP-7825 gas cap#309

Open
kupermind wants to merge 6 commits into
mainfrom
feat/proposal-24-split-under-gas-cap
Open

Split failed proposal 24 into proposal 24 (de-whitelist + guard) and proposal 25 (un-nominate), under the EIP-7825 gas cap#309
kupermind wants to merge 6 commits into
mainfrom
feat/proposal-24-split-under-gas-cap

Conversation

@kupermind

@kupermind kupermind commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Why

The original single 41-action bundle (submitted as proposal 24) reverted on-chain: 0x540c…20f failed with status 0, gasUsed 16,530,883 against gasLimit 16,777,216.

That gas limit is exactly 2²⁴ = 16,777,216 — the EIP-7825 (Fusaka) per-transaction gas cap. The bundle's execute() needs ~19.9M gas (profiled on a mainnet fork), which no single transaction can supply. Every simulation (forge fork tests, Safe/Tenderly) passed because they run with a high gas allowance and none enforce the per-tx cap — only mainnet consensus does.

A GovernorOLAS execute() is atomic, so the bundle can't be executed in pieces and re-submitting fails identically. The fix is to split it into two independent, sub-cap proposals.

Where the gas went (mainnet-fork profile)

group gas note
4× OP-stack sendMessage (Optimism/Base/Celo/Mode) ~9.57M ~2.39M each — L1 gas-burn ∝ the 2M minGasLimit for a trivial L2 sstore
32× removeNominee ~9.55M ~298k each (routes through the Dispenser)
Gnosis/Polygon/Arbitrum/mainnet de-whitelist + GuardCM ~0.74M
total ~19.9M over the 16,777,216 cap

What

Two independent proposals, each measured (pure Timelock execute) well under the cap:

  • Proposal 24 — scripts/proposals/proposal_24_dewhitelist_and_guard/ (9 actions, ~10.4M gas): de-whitelist the 8 same-address multisig adapters (mainnet + 7 L2 bridges; Polygon carries two adapters) + extend the GuardCM allowlist with 19 emergency-pause/drain triples. Carries the Arbitrum retryable value.
  • Proposal 25 — scripts/proposals/proposal_25_unnominate/ (32 actions, ~9.2M gas): VoteWeighting.removeNominee for the 32 retired staking nominees. All direct L1 calls — no L2 propagation.

Bridge encodings, the GuardCM batch, and the Arbitrum retryable params are byte-for-byte identical to the original bundle, so L2 delivery is unchanged; only the L1 bundling split. Each proposal's description is self-contained and context-specific (24 = security hardening + guard; 25 = staking nominee cleanup).

proposalIds

  • 24: 0xa670d15c929979d29c817dd94cf28995248de5b23b68dfa3403b624f173a125c
  • 25: 0xdaea1b75e72e86880322782b4a8a4a022992711bbeba32df08bc1d67feebbca6

Tests — all green (mainnet + L2 forks)

  • test/Proposal24ForkL1.t.sol — full propose → vote → queue → execute through the live GovernorOLAS; asserts the mainnet de-whitelist + all 19 GuardCM triples; Arbitrum retryable executor-funded. 3 pass
  • test/Proposal24ForkL2OpStack.t.sol — Optimism/Base/Celo/Mode delivery. 4 pass
  • test/Proposal24ForkL2Other.t.sol — Gnosis (AMB) / Polygon (FxRoot, both adapters) / Arbitrum (aliased Timelock). 3 pass
  • test/Proposal25ForkL1.t.sol — full lifecycle; asserts all 32 nominees removed. 3 pass

Pure-execute gas from the fork tests: proposal 24 execute = 10,380,834, proposal 25 execute = 9,163,012 — both comfortably under 16,777,216.

Timing note (proposal 25)

removeNominee reverts within the last week of the ongoing Tokenomics epoch (block.timestamp >= getEpochEndTime(eCounter-1) + epochLen - 1 week). The full propose→execute wall-clock is ~6.4 days (votingDelay + votingPeriod ≈ 4.55 d + governorDelay ≈ 1.82 d, not the Timelock minDelay of 0). Proposal 25 must be timed so execution lands in an epoch's first-week window.

Cleanup

Removes the obsolete single-bundle proposal folder and its three fork tests (preserved in git history). The failed proposal can never be re-executed.

Note on MIN_GAS

The 2M minGasLimit on the four OP-stack sends (a trivial L2 sstore) accounts for ~9.6M of the original total and is intentionally left unchanged here, to keep proposal 24's bridged payloads byte-identical to the already-simulated original bundle. It could be trimmed in a future revision for cheaper execution; the split alone resolves the cap violation.

kupermind and others added 2 commits July 3, 2026 20:30
…-tx gas cap

The original single 41-action proposal 24 reverted on-chain
(tx 0x540c2026c0a122465806240becdd8381489e19e61d74e74ec184a9660621e20f):
its execute() needed ~19.9M gas while EIP-7825 (Fusaka) caps a single
transaction at 2^24 = 16,777,216 gas. Every simulation passed because
forge/Tenderly/Safe run with a high gas allowance and none enforce the
per-tx cap.

Split the bundle so each proposal's execute stays well under the cap
(measured on a mainnet fork, pure Timelock execute):
  - 24a — de-whitelist the same-address multisig adapters (8) + extend the
    GuardCM allowlist (19 triples). 9 actions, ~10.4M gas.
  - 24b — un-nominate the 32 retired staking contracts. 32 actions, ~9.2M gas.

Bridge encodings, the GuardCM batch, and the Arbitrum retryable params are
byte-for-byte identical to the original proposal 24, so the L2 delivery is
unchanged; only the L1 bundling differs.

Fork tests (all green): full propose -> vote -> queue -> execute lifecycle
through the live GovernorOLAS for each proposal, plus per-L2 bridge-delivery
sims for 24a (Gnosis/Polygon[two adapters]/Arbitrum/Optimism/Base/Celo/Mode).
24b is entirely L1 (VoteWeighting tracks all chains via the chainId arg), so
it has no L2 propagation.

Removes the obsolete single-bundle proposal_24 folder and its fork tests
(preserved in git history); the failed proposal can never be re-executed.

proposalId 24a: 0xa670d15c929979d29c817dd94cf28995248de5b23b68dfa3403b624f173a125c
proposalId 24b: 0xdaea1b75e72e86880322782b4a8a4a022992711bbeba32df08bc1d67feebbca6

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(un-nominate)

The two halves of the split are independent proposals, so give them distinct
DAO numbers: proposal 24 = de-whitelist same-address multisigs + extend the
GuardCM allowlist; proposal 25 = un-nominate the 32 retired staking contracts.

Pure rename of folders, files, contracts, tests and prose; the DESCRIPTION
constants and description.txt are untouched, so the proposalIds are unchanged:
  - proposal 24: 0xa670d15c929979d29c817dd94cf28995248de5b23b68dfa3403b624f173a125c
  - proposal 25: 0xdaea1b75e72e86880322782b4a8a4a022992711bbeba32df08bc1d67feebbca6

Each description already matches its own context (24 = security hardening +
guard; 25 = staking nominee cleanup); neither references the other or a number.
Build + all 13 fork tests green; proposalIds re-verified after the rename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kupermind kupermind changed the title Split proposal 24 into 24a + 24b to fit under the EIP-7825 per-tx gas cap Split failed proposal 24 into proposal 24 (de-whitelist + guard) and proposal 25 (un-nominate), under the EIP-7825 gas cap Jul 4, 2026
Self-contained, color-coded, collapsible HTML that DECODES each proposal's
authoritative Forge-builder calldata (calldata.json) and re-derives the
proposalId from it, so the artifact cannot drift from what is voted:
  - proposal 24: 0xa670d15c929979d29c817dd94cf28995248de5b23b68dfa3403b624f173a125c
  - proposal 25: 0xdaea1b75e72e86880322782b4a8a4a022992711bbeba32df08bc1d67feebbca6

annotate.js auto-groups entries by selector, so the one generator renders both
(24 = de-whitelist + GuardCM; 25 = removeNominee). Each folder gets annotate.js,
calldata.json and the rendered <folder>.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@77ph 77ph left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audit review — requesting changes (one blocking item; the split itself is correct).

The gas-cap split is sound and well done: the root cause is correctly diagnosed (the original 41-action bundle reverted at gasUsed 16,530,883 against gasLimit 16,777,216 = 2²⁴, the EIP-7825/Fusaka per-tx cap; the note that forge/Safe/Tenderly simulations all pass because none enforce the per-tx cap is the sharp catch), the 41 → 9 + 32 split is faithful with byte-for-byte-identical encodings, and the fork tests are green. Proposal 24 (same-address adapter de-whitelists + GuardCM extension) is good.

Blocking — proposal 25 (removeNominee × 32). I checked the current weight of all 32 nominees on the live VoteWeighting (0x9541…c5c1):

  • 31 of 32 have zero current weight → clean removals.
  • 1 — n/a id2 (Base, chainId 8453, 0x51c5f4982B9b0B3c0482678f5847EA6228Cc8E54) — still carries a substantial active vote (~64% of getWeightsSum).

Removing a nominee that still has active votes leaves orphaned voting power in the sum accounting (governance vulnerabilities-list item #8). Please exclude n/a id2 from proposal 25: execute the 31 zero-weight removals now, and handle n/a id2 separately — e.g. have its voter(s) revokeRemovedNomineeVotingPower first so its removal is a clean zero-weight case. This is only about the proposal-25 nominee set; the split mechanics are unaffected. I'll approve once n/a id2 is out of the batch.

kupermind and others added 2 commits July 6, 2026 15:18
Add Part 3 to proposal 24: shut down every OLAS-payment mech factory by
calling setMechFactoryStatuses([olasFactory], [false]) on the Mech Marketplace
of each network where an OLAS (fixed-price-token, PAYMENT_TYPE 0x3679...45e9)
factory is currently whitelisted. Once removed, no new mech that takes payment
in OLAS can be created, so nothing can ever route a payment to an OLAS
balanceTracker (the tracker mapping is unreachable; it cannot be unset on-chain
because setPaymentTypeBalanceTrackers reverts on address(0)).

Scope re-derived from live on-chain state + deployment globals, not the stale
side analysis. configuration.json mislabels the USDC factory as
MechFactoryFixedPriceToken on Ethereum/Arbitrum/Celo; mechFactoryFixedPriceTokenOLASAddress
gives exactly one OLAS factory per chain. All 7 are whitelisted right now
(mapMechFactories == true), including Ethereum and Gnosis (both activated after
the earlier analysis), each Mech Marketplace owned by the same governance
mediator as its ServiceRegistryL2:
  - Ethereum  0xddF6...26fD  (direct L1)
  - Gnosis    0x31ff...45FA  (AMB)
  - Polygon   0xa0DA...0f82  (FxRoot)
  - Arbitrum  0x70A0...2bcf  (retryable)
  - Optimism  0x26Ea...684c  (OP-stack)
  - Base      0x9737...9BEe  (OP-stack)
  - Celo      0xA123...21E6  (OP-stack)

Proposal 24 is now 16 actions. The 3 OP-stack factory-disable sends use a small
FACTORY_MIN_GAS (300k; measured L2 delivery ~65k) so execute stays under the
EIP-7825 cap. Governor.execute() = 12,634,309 gas vs the 16,777,216 cap
(24.7% headroom), asserted in the L1 lifecycle fork test.

Content changed, so proposal 24's id changes:
  old 0xa670d15c929979d29c817dd94cf28995248de5b23b68dfa3403b624f173a125c
  new 0xc59c9567b77860836814a2144dcd1523fe8811248ea6ac94ed45f67cff8c1f0e
description.txt matches the DESCRIPTION constant byte-for-byte; the id is
reproduced from calldata.json + description.txt.

Fork tests (all green): L1 lifecycle + Timelock execute; per-L2 factory-disable
delivery for Optimism/Base/Celo (OP-stack) and Gnosis/Polygon/Arbitrum.
annotate.js groups the new setMechFactoryStatuses entries and the HTML
breakdown is regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sage per L2

Both effects for the same L2 chain (Part 1 same-address multisig de-whitelist
+ Part 3 OLAS mech-factory de-whitelist) now ride in ONE bridge message. Each L2
BridgeMediator loops over concatenated (target,value,len,payload) tuples, so:
  - Gnosis (HomeMediator): 2 tuples in one requireToPassMessage
  - Polygon (FxGovernorTunnel): 3 tuples (2 adapters + factory) in one sendMessageToChild
  - Optimism/Base/Celo (OP-stack): 2 tuples in one sendMessage
Arbitrum is the exception — a retryable targets a single L2 address with no
looping mediator, so ServiceRegistryL2 and the Mech Marketplace need two Inbox
retryables. Mode has no OLAS factory (de-whitelist only). Mainnet stays two
direct Timelock calls.

This collapses 16 actions to 11 (drops 5 redundant bridge sends) and cuts
Governor.execute() from 12,634,309 to 10,602,040 gas (~2.03M saved; now 36.8%
under the 16,777,216 EIP-7825 cap). Follows the established multi-tuple packing
pattern (see proposal_22 FxRoot batching).

description.txt is unchanged (same three effects), so only the calldata batching
differs; proposalId changes accordingly:
  old (16-action) 0xc59c9567b77860836814a2144dcd1523fe8811248ea6ac94ed45f67cff8c1f0e
  new (11-action) 0x0f66ff0be88382e028474de0c50f59ac0c4bd98579e8f637c2d587aabc6fa62d

Fork tests (all green): L1 lifecycle asserts execute < cap; per-L2 COMBINED
delivery asserts BOTH the adapter and the OLAS factory flip to false in one
message (Gnosis/Polygon/Optimism/Base/Celo), Mode de-whitelist only, and the two
Arbitrum retryables. annotate.js gains a "combined" group; HTML regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@77ph

77ph commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Re-review of the post-review commits (bdddb0ee9 — de-whitelist OLAS mech factories on 7 chains; 4ead3130a — per-chain message batching) — verified first-hand.

Gas (the constraint that reverted the original 41-action bundle) — both proposals sit comfortably under the EIP-7825 per-tx cap (16,777,216):

Proposal execute() gas vs. cap margin
24 (11 actions) 10,602,040 — asserted < cap in Proposal24ForkL1 ~6.18M
25 (32 removeNominee) 9,163,050 — measured via the execute-as-Timelock path ~7.6M

Fork tests: 14/14 pass (6 L1 + 8 L2) — full propose → vote → queue → execute on a mainnet fork for both proposals, and L2 combined-message relay+execute across all 7 chains. The batched per-chain messages (registry de-whitelist + factory de-whitelist in a single bridge send) execute for ~44k–160k gas — far under the OP-stack MIN_GAS = 2,000,000. (test_L2_gnosis needs an archive-capable fork RPC — https://rpc.gnosischain.com — where it passes; the earlier failure was RPC state availability, not a code issue.)

On-chain authorization re-verified on all 7 chains: each Mech Marketplace owner() == the claimed governance mediator, and each OLAS factory is currently whitelisted (mapMechFactories == 1), so every setMechFactoryStatuses([factory], [false]) is a correctly-authorized, meaningful de-whitelist. The cross-chain address coincidences (e.g. MF_POLYGON == the mainnet Treasury address, SRL2_BASE == the mainnet Timelock address) are deterministic-deployment artifacts — the contracts exist and behave correctly at those addresses on their own chains.

Minor (test parity): Proposal25ForkL1 has no explicit assertLt(executeGas, 16777216), unlike Proposal24ForkL1. The current execute is ~9.16M with large headroom, but the test does not guard against a future regression (e.g. more nominees). Suggest adding a symmetric gas-cap assertion to the proposal-25 fork test.

Blocker unchanged — requesting changes stands. Re-scanned the current weight of all 32 nominees on the live VoteWeighting: 31 are zero-weight; one — n/a id2 (Base 8453, 0x51c5f4982B9b0B3c0482678f5847EA6228Cc8E54) — still holds ~64% of getWeightsSum. Removing an active-vote nominee leaves orphaned voting power (vulnerabilities-list item #8). Please still exclude n/a id2 from proposal 25 — execute the 31 zero-weight removals now, and handle n/a id2 separately (e.g. revokeRemovedNomineeVotingPower on its voter(s) first, so its removal becomes a clean zero-weight case).

Note (monitoring, not a blocker): once proposal 24 executes, the OLAS-payment balance trackers on these 7 chains become retired branches — per the autonolas-marketplace vulnerabilities-list #17, deposits to a retired tracker are stuck (no payment path and no requester-withdrawal path), so a non-zero balance there should be read as stuck funds, not payment activity.

Adds a fork test that verifies and replays the REAL proposal 24 submission
(tx 0xd543cd06415529a021ef30be0124af31f318ed61adb604e0aae92647941184ac,
block 25,476,048, proposer Safe 0x3447...0039). It forks mainnet at latest,
finds the already-created proposal by its on-chain id
0x0f66ff0be88382e028474de0c50f59ac0c4bd98579e8f637c2d587aabc6fa62d (NOT a
re-propose), reconstructs the queue/execute args via buildProposal() and
requires their hash to equal the submitted proposalId, then drives it through
vote -> queue -> execute.

Asserts every L1 effect lands (mainnet same-address multisig + OLAS mech factory
de-whitelisted, 19 GuardCM triples set, Timelock retains no funds) and that
Governor.execute() = 10,588,589 gas stays under the 16,777,216 EIP-7825 cap.

The replay reads the live still-Pending proposal and simulates its fixed voting
window (blocks 25,489,139-25,508,775), so it self-skips once on-chain voting
starts; the durable executability proof is Proposal24ForkL1Test (proposes fresh,
independent of the live proposal's timeline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants