feat(protocol): raise the inbox basefee sharing percentage to 100% (Proposal0026) - #22127
davidtaikocha wants to merge 26 commits into
Conversation
…age to 100% Deploys a MainnetInbox whose basefeeSharingPctg is 100 instead of 75 and upgrades the mainnet inbox proxy to it: one upgradeTo from the DAO controller, no L2 leg, no initializer, immutables only. - MainnetInbox.sol: basefeeSharingPctg 75 -> 100. - LibL1Addrs.ZK_REQUIRED_VERIFIER: the live proof verifier (Proposal0019), so the deploy script reproduces the live immutables from the library. - DeployInboxUpgradeL1: reads the live proxy's getConfig() before broadcasting and aborts unless the new implementation differs from it only in the percentage. - Proposal0024: MAINNET_INBOX_NEW_IMPL is a placeholder until the implementation is deployed; the print mode reverts until then and no action file exists. - Tests pin the encoding, the placeholder phase, the full live configuration as literals, and rehearse the upgrade on a mainnet fork. - Runbook with a TODO for @dantaik on the rationale. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ementation MainnetInbox 0xA18431d42C8dF9778905fBEa912aCF1881b49D2e was deployed on 2026-09-12 in L1 block 25,961,745 by DeployInboxUpgradeL1, with LibForcedInclusion and LibInboxSetup linked (three creates, all with status 1). Its getConfig() equals the live proxy's except the sharing percentage, Etherscan verified it, and its creation code is reproduced byte for byte from this branch. - Proposal0024.MAINNET_INBOX_NEW_IMPL and the test's DEPLOYED_INBOX_IMPL literal are filled in; the placeholder-phase branches are gone. - Proposal0024.action.md generated with `P=0024 pnpm proposal` and pinned by test_actionFileMatchesTheBuiltCalldata; the L1 dry run reverts DryrunSucceeded(); the fork rehearsal executes the committed calldata against the deployed implementation and deploys nothing. - Runbook: deployment facts, addresses, codediff link, and a creation-code comparison in place of forge verify-bytecode, which refuses library-linked contracts. - Deploy script doc: three creates, not two. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…runbook Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… refresh fix Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ences Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… stack limit genesis-docker compiles test/layer1 under the via-IR layer1o profile and failed with "Variable size_1 is 1 too deep in the stack". The action loop in Proposal0024Fork.t.sol::_executeAs is identical to Proposal0023's, but it has a single caller, so the IR inliner folds it into the test, whose live variables push the loop's call temporaries and the concatenated assertion message one slot over the limit. The failure is now a custom error carrying the action index, which needs no string.concat or vm.toString temporaries; the layer1o build passes and the rehearsal still passes against live mainnet state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the rationale TODO with the case for moving the inbox basefee share from 75% to 100%, and make the runbook precise about where the non-coinbase share actually goes. - Name the two recipients exactly: the coinbase, which both drivers overwrite with the proposal's `proposer` at derivation, so it is always the whitelisted preconfer that proposed the block; and the Anchor contract, which holds its share as a plain ETH balance until the DAO sweeps it with `Anchor.withdraw` through the L2 delegate controller. The runbook previously called the Anchor "the L2 treasury" without saying that nothing forwards the balance anywhere. - State the argument as the asymmetry it is: at current L2 volume the 25% is immaterial to the DAO, while for a proposer it is a per-transaction loss on every sponsored transaction that makes fee sponsorship a business nobody would enter. - Record the two limits of the refund: it is exact only within a preconfer's own proposals, and it covers the L2 fee only. - Add a trade-offs section covering the forgone revenue and its reversibility, and the fact that a proposer's own L2 gas round-trips fully at 100 so L1 data cost becomes its only floor. - Note that the already-accrued Anchor balance is untouched and stays withdrawable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhqmeYDadT95CmcUhWzc2B
…100% Match `MainnetInbox` so local and devnet deployments behave like mainnet after Proposal0024 executes. `DevnetInbox` is not in `MainnetInbox`'s dependency tree and no deployed contract reads it; its only consumer is `DeployProtocolOnL1`. The taiko-client integration tests deploy through that script, and two of them asserted the treasury balance strictly grows, which only holds while the percentage is below 100. Derive the expectation from the inbox's own `basefeeSharingPctg` instead, so both tests are correct at any setting: at 100 the treasury gains nothing and the per-transaction reconciliation in `TestTreasuryIncome` still pins the split exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhqmeYDadT95CmcUhWzc2B
`LibL1Addrs.ZK_REQUIRED_VERIFIER` became `ZKEVM_VERIFIER`, but two call sites still referenced the old name and no longer compiled: `DeployInboxUpgradeL1._checkLiveProxy` and the Proposal0024 test that builds `MainnetInbox` with the live address immutables. The address is unchanged. Also update the name in the Proposal0024 config table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhqmeYDadT95CmcUhWzc2B
Restore `LibL1Addrs.ZK_REQUIRED_VERIFIER` and its three call sites in `DeployInboxUpgradeL1`, the Proposal0024 test and the Proposal0024 config table. The address `0x7284aaC05555Ae6559bdAd8B4221eC9584254Eec` is unchanged, and the name now matches the deployed contract (`ZkRequiredVerifier`), Proposal0019's own constant and the L1 deployment log, so nothing in the tree still says `ZKEVM_VERIFIER`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhqmeYDadT95CmcUhWzc2B
…UpgradeL1 `_checkLiveProxy` only rejected a live percentage already equal to the new value, so any other non-100 reading passed the guard — and `_checkConfig` cannot catch it either, because it normalises `basefeeSharingPctg` away before comparing the new implementation with the live one. The NatSpec and the Proposal0024 runbook both claimed the script aborts unless the live value is still 75, which was not what the code did. Add `OLD_BASEFEE_SHARING_PCTG = 75` and require the live proxy to equal it, after the existing `AlreadyUpgraded` check so a re-run following execution still reports itself rather than as a generic mismatch. Reported by the deepseek-review bot on the pull request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhqmeYDadT95CmcUhWzc2B
|
Went through the deepseek-review findings. Took the warning, declined both suggestions. Warning — The guard only rejected a live percentage already equal to Suggestion — named constants for 75 in the fork tests. Declining.
Suggestion — pin the fork block in This is the repo's existing convention rather than an oversight. Generated by Claude Code |
|
Second deepseek-review pass. Checked all four against the code; none needs a change, and one is factually wrong about what the script does. Warning 1 — "broadcasts before validating the full config", leaving an orphaned implementation. Not how
Warning 2 — library deployment is implicit. Correct, and deliberately left alone. Forge's implicit linked-library deployment is exactly what ran on 2026-09-12, and the runbook documents the resulting three creates with Suggestion 1 — split the address check from Suggestion 2 — Generated by Claude Code |
…sal IDs (#22154) Co-authored-by: Claude <noreply@anthropic.com>
…-basefee-sharing-100
Merge commits are not allowed on this repository
🐋 DeepSeek Code Review🟡 Warnings
🔵 Suggestions
🟢 What Looks Good
Automatically triggered on PR update • model: |
Pull request was converted to draft
Renames the basefee-sharing proposal from 0025 to 0026 across its script, proposal doc, action file, tests and harness, and updates the references in `MainnetInbox.sol`, `DeployInboxUpgradeL1.s.sol` and the layer1 gas report. Rename only: no calldata, address or configuration value changes. The action file's calldata is unchanged, and the gas report was regenerated with `forge snapshot` rather than hand-edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhZUYdta3M1mhVs7VzVFyZ
What
Proposal0026 raises the mainnet Shasta inbox's
basefeeSharingPctgfrom 75 to 100, so the whole L2 basefee of every block in a proposal made after execution goes to that block's coinbase and nothing is retained by the L2 fee treasury, the Anchor contract0x1670000000000000000000000000000000010001. The percentage is a constructor immutable of the inbox implementation, so the proposal is oneupgradeToon the inbox proxy0x6f21C543a4aF5189eBdb0723827577e1EF57ef1f, executed by the DAO controller that owns it. 1 L1 action, no L2 leg, no initializer, immutables only.Under
contracts/three things move:MainnetInbox.sol:basefeeSharingPctg: 75→100. This is the change the proposal ships.DevnetInbox.sol: the same flip, so local and devnet deployments match mainnet. See below.LibL1Addrs.ZK_REQUIRED_VERIFIER: the live proof verifier0x7284aaC0…(Proposal0019'sZkRequiredVerifier), which was only aProposal0019constant until now, so the deploy script reproduces all five live immutables from the address library.The rest is the deploy script, the proposal, the action file, the runbook and tests.
BuildProposal.solis untouched.Why this PR touches
DevnetInboxandsyncer_test.goDevnetInbox.solmoves tobasefeeSharingPctg: 100as well, so local and devnet deployments behave like mainnet after execution. It is not inMainnetInbox's dependency tree and no deployed contract reads it; its only consumer isDeployProtocolOnL1.That is what pulls
packages/taiko-client/driver/chain_syncer/event/syncer_test.goin. The taiko-client integration tests deploy the protocol throughDeployProtocolOnL1, and two of them —TestTreasuryIncomeandTestTreasuryIncomeAllAnchors— asserted the treasury balance strictly grows. That only holds below 100: at 100 the coinbase takes the whole basefee and the treasury receives nothing. Both now derive the expectation from the inbox's ownbasefeeSharingPctg, so they are correct at any setting, and the per-transaction reconciliation inTestTreasuryIncomestill pins the split exactly.Kept here rather than split out, because it is load-bearing for the devnet flip in this PR: without it, merging this breaks
main. Splitting them would only create a merge-order dependency — the test change would have to land first ormaingoes red — for no benefit, since the change is a no-op at 75.Note that this PR's own integration lanes do not currently exercise the flip.
taiko-client--test.ymlchecks the protocol out frommainrather than from the PR, so they deployDevnetInboxat 75 and only ever take the< 100branch of the new assertions. #22142 fixes that; once it lands, mergingmainin here makes these lanes deploy this PR's contracts at 100.The new implementation is the live one with one field changed
DeployInboxUpgradeL1read the live proxy'sgetConfig()before broadcasting and would have aborted unless its five addresses were theLibL1Addrsconstants it compiles in and its percentage still exactly 75 (OLD_BASEFEE_SHARING_PCTG); after deploying it compared the new implementation'sgetConfig()with the live one and would have aborted unless the percentage was the only difference. Read back on-chain at block 25,961,770: the new implementation'sgetConfig()equals the live proxy's in every field butbasefeeSharingPctg(100 vs 75), it has 23,058 bytes of code, andproxiableUUID()returns the EIP-1967 implementation slot, which is whatupgradeTochecks.test_mainnetInbox_MatchesTheLiveConfigExceptForBasefeeSharingpins every field of the live configuration as a literal against aMainnetInboxbuilt from this tree.Diffing the implementation's dependency tree (
MainnetInbox.soland its 23 imports undercontracts/) from the commit the live implementation was built from (9078278909a43a83fc5bb2664f30b81eb5c967f6, Proposal0019) tomainchanges two files:MainnetInbox.sol(the literal, and theLibFasterReentryLockoverrides that #22058 moved into the base contract) andEssentialContract.sol(#22058: the reentry lock in transient storage at the byte-identical_REENTRY_SLOT0xa5054f72…;__reentrybecomesprivateand keeps its slot). Nothing undercontracts/layer1/core/changed.MainnetInbox_Layout.solis identical at both commits,_initializedis 3 andInboxhas noinit4, the ABI and theProposedevent are unchanged, andLibInboxSetup.validateConfigallows<= 100, so 100 is the maximum rather than an edge case; both execution clients computefee × pctg / 100, so the treasury remainder is exactly 0.Evidence
MainnetInbox, solc 0.8.30, osaka, optimizer 200 runs). Independently, the creation code is reproduced byte for byte from this branch:forge build, patch the two link references with the deployed library addresses, append the constructor arguments, and the result equals the deployment transaction's input (24,947 bytes).forge verify-bytecodecannot do this for a library-linked contract ("Unlinked bytecode is not supported"), and compiling with--librarieschanges the metadata hash, so the runbook documents the manual comparison instead.Proposal0026Fork.t.sol, gated onL1_FORK_URL, passing on 2026-09-12 through a public RPC after the deployment): asserts the proxy still runs the Unzen implementation0x5253D4C9…, executes the committed calldata from the DAO controller against the deployed implementation, and asserts the proxy answers 100 with every other configuration field, the core state, the last and last-finalized proposal hashes, the forced-inclusion queue and fee, the owner, the activation timestamp and the initializer version unchanged. Nothing is deployed by the test. A second test runsController.dryrunon the fork and expectsDryrunSucceeded().P=0026 pnpm proposal:dryrun:l1simulated against the deployment RPC reverts withDryrunSucceeded(), the success signal.upgradeTo, the revert on a missing implementation, the absence of an L2 leg, that the no-argument builder forwards the deployed address (aDEPLOYED_INBOX_IMPLliteral, so a library edit cannot be mirrored silently), the full live configuration as literals, and thatProposal0026.action.mdmatches what the code builds.Client rollout (runbook, Client rollout)
The drivers derive the byte from the
Proposedevent and provers take it from the event they verify, so neither needs a change. The whitelisted preconfer (Catalyst) cachesgetConfig().basefeeSharingPctgat startup, so it must be restarted right after execution: until then every block it preconfirms is re-derived with 100 once proposed (same transactions, differentextraDataand state root), i.e. one preconfirmation reorg per block. Blocks preconfirmed under 75 but carried by the first post-execution proposal are re-derived the same way, once; executing right after a proposal lands bounds that to a few blocks.Review notes
Proposal0024.s.solandDeployERC20VaultUpgradeL1.s.sol.MainnetInboxlinks two libraries (LibForcedInclusionandLibInboxSetupboth havepublicfunctions), so the deployment was three creates, all in one block with status 1; the receipts were checked rather than forge's summary line.Proposal0023#22120 pattern: a separate PR with the post-execution entries pre-written asTBD, filled after execution.DeployInboxUpgradeL1must not be re-run; it also refuses to run once the live proxy answers 100.Next steps
Proposal0026.action.md(target: DAO controller,execute(bytes)).🤖 Generated with Claude Code