feat: Safe multisig execution mode, plus on-chain chain/pool removal#7
Open
aelmanaa wants to merge 1 commit into
Open
feat: Safe multisig execution mode, plus on-chain chain/pool removal#7aelmanaa wants to merge 1 commit into
aelmanaa wants to merge 1 commit into
Conversation
|
👋 aelmanaa, thanks for creating this pull request! To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team. Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks! |
MODE=safe emits each operation's Call[] as a reviewable Safe Transaction Builder batch and optionally executes it via execTransaction with raw owner signatures; emitted batches compose into one atomic Safe transaction (ExecuteBatch). Includes an EIP-712 safeTxHash recompute cross-checked against the Safe, a canonical v1.4.1 CREATE2 deploy helper, byte-equality and ceremony fork tests (incl. Safe v1.5.0), and docs/governance-modes.md. Default EOA behavior is unchanged. Also adds on-chain lane teardown. RemoveChain.s.sol fully unsupports a remote chain, version-dispatched across 1.5.0/1.5.1/1.6.1/2.0.0: 1.5.0 uses the single-argument allowed:false shape, 1.5.1+ the modern toRemove[] shape, with an isSupportedChain pre-check and a version-safe read before dispatch. RemoveRemotePool gains a 1.5.0 redirect, since 1.5.0 holds one remote pool per chain and has no standalone pool removal. The Safe byte-equality catalog covers both removal shapes, so Safe executes them byte-identically to the EOA path. docs/pool-versions.md documents the two teardown operations, the live-lane drain sequence, the reciprocal half-open window, and the last-pool footgun.
0048fd8 to
2b93f55
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.
What
Adds a Safe multisig execution mode on top of the shared action layer, including composition of
multiple operations into one Safe transaction. The
MODEenvironment variable selects the executorfor the
Call[]every write script already builds:MODEunset (oreoa): today's behavior, unchanged: same commands, same flags, same broadcastpath, same output. No existing README command changes and no new required env var.
MODE=safe: the identicalCall[]is emitted as a Safe Transaction Builder JSON batch underbatches/for review and signing (importable in the Safe{Wallet} UI, proposable viasafe-cli).SAFE_EXEC=directadditionally executes it viaexecTransactionwith raw ECDSA ownersignatures; this works on any EVM chain, with no dependency on the hosted Safe Transaction
Service.
with one review artifact, one
safeTxHash, one signing round, one nonce:Why
A production issuer cannot run privileged operations from a single EOA. Because every operation is
already a pure
Call[]builder (CctActions), Safe support is an executor concern only: nooperation is re-implemented, and byte-equality tests prove the Safe batch carries exactly the
calldata the EOA path broadcasts, so reviewing one mode reviews them all.
How
src/base/EoaExecutor.s.sol:executeCallsdispatches onMODE(defaulteoa, exact previousbody);
executingAccount()resolves the account acting on-chain (the Safe in safe mode) soauthority preflights work in both modes. All write scripts inherit the switch with zero
per-script changes.
src/base/SafeMode.sol: the Safe executor: review logging, batch emission, and the directexecTransactionpath (nonce read, EIP-712 hash recompute cross-checked against the Safe'sgetTransactionHash, signatures packed sorted ascending by signer address).src/base/SafeTxHash.sol: independent EIP-712safeTxHashrecompute. The SafeTx type stringends in
uint256 nonce; encoding the Solidity parameter name_nonceinstead yields a hash thatnever matches signer devices; a regression test pins this.
src/base/SafeBatchEmitter.sol/src/base/SafeBatchLoader.sol: canonical Transaction BuilderJSON out and back in (the loader validates chain id, Safe address, and non-emptiness;
loadManyconcatenates files in execution order).
script/governance/ExecuteBatch.s.sol: composes emitted batches (BATCH_FILES, requiredBATCH_NAME) and hands the merged batch to the standard Safe executor.script/governance/DeploySafe.s.sol: canonical v1.4.1 CREATE2 deploy; predicts the address,asserts the deployment lands on it, idempotent: the same owners/threshold/salt reproduce the
same Safe address on every chain.
script/setup/ClaimAndAcceptAdmin.s.sol: the claim+accept registration pair as one atomicoperation, so registration can ride in a deferred batch.
docs/governance-modes.md: the architecture doc (mode switch, both Safe paths, thereview-before-submit gate,
safeTxHashmechanics, independent signature verification, batchingrecipe and composition rules, Transaction Service support matrix).
On-chain chain/pool removal
Also adds on-chain lane teardown alongside the Safe work:
script/configure/remote-chains/RemoveChain.s.sol: fully unsupports a remote chain, version-dispatched across1.5.0/1.5.1/1.6.1/2.0.0.1.5.0uses the single-argumentallowed: falseshape;1.5.1+uses the moderntoRemove[]shape, with anisSupportedChainpre-check and a version-safe read before dispatch.script/configure/remote-pools/RemoveRemotePool.s.sol: gains a1.5.0redirect, since1.5.0holds one remote pool per chain and has no standalone pool removal.docs/pool-versions.md: documents the two teardown operations, the live-lane drain sequence, the reciprocal half-open window, and the last-pool footgun.Tests
test/governance/(Sepolia fork, real Safe deployed from the canonical stack insetUp):compositions all round-trip to the identical
to/value/datathe EOA mode broadcasts.execTransactionpath end-to-end: ownership handed to the Safe (accept executed throughthe Safe), then
applyChainUpdatesexecuted by the Safe lands the identical end state as theEOA path; a full CCT setup (accept ownership + registration pair + setPool + applyChainUpdates)
runs as one
execTransactionconsuming a single nonce.GS013) with zero partial state; unsortedsignature packs are rejected (
GS026).safeTxHashrecompute == on-chaingetTransactionHash; the_noncetypehash regression;composition guards (chain-id mismatch, foreign Safe, empty batch).
(about 20% on a five-operation setup from live receipts, roughly 685k versus 545k).
fuzzed hash cross-check and the full flow (single + batched). The default stays v1.4.1: it is
what the Safe{Wallet} UI deploys, and only the 1.4.1 stack currently has the complete canonical
rollout for cross-chain address mirroring.
Full suite: 74 passed / 0 failed (all 48 pre-existing tests untouched).
forge fmt --checkandforge lintclean.Transaction Service probe (2026-07-10): Sepolia
sep200, Base Sepoliabasesep200, MantleSepolia
mnt-sep200 (newly served), Avalanche Fuji / Ink Sepolia / 0G Galileo / Plume testnet 404.A 404 gates only the Transaction Service path; the direct path works everywhere.
Live testnet validation
Sepolia + Avalanche Fuji, existing 2-of-3 Safe
0xf55FEba2D9c767Bf8018362d9eE4F378dcEB727E. Allacceptance items passed against throwaway, explorer-verified fixtures on both chains (Sepolia:
token
0x48d94D0a2d6182fEA98143d588cd79A019bF11B3, pool0x03A2e316D205c90fe412523c4F8B3Cc6DD12D6c5; Fuji: token0x3CDff159eb74a9c30FE44a8813255228000e283e, pool0x6d9300Fa54e023Ee04F7a4680A385396d4A9Ac0f):MODEunset (dry run, then broadcast); zero newenv vars, behavior unchanged.
execTransactionpath on both chains: accept-ownership and rate-limit changes; finalstate equals the EOA-applied state exactly; the submitter was a non-owner. On Fuji the
Transaction Service returns 404, so the direct path is the only Safe path there, validating it
as the universal fallback.
safeTxHashthree ways: script recompute ==getTransactionHash== independent tool, on bothchains (Sepolia nonce 23
0xce4568d561eea90aef5b1baa9a5c0f63df882f811d5b3d8183b4b9f2ca756ed9; Fuji nonce 250x583a8f2519a7e92ba76d283b566f8f733b2712570b33d653e23b616002a803ec).cast calldataexpectation byte-for-byte before anything was submitted.
safe-cli1.9.0,visible in the Safe{Wallet} queue, executed at nonce 24
(exec tx);
the tx-service proposal
dataand theMODE=safebatchdataare byte-identical for the sameoperation. (safe-cli 1.9.0's
executeunderprices EIP-1559 gas on Sepolia; the permissionlessfallback is documented in
docs/governance-modes.md.)getCCIPAdmin,defaultAdmin,rateLimitAdmin,feeAdminall moved to the Safe on both chains; a deployer-EOA attempt at agated operation reverts
Unauthorized; live CCIP token transfers ran in both directions throughthe Safe-configured lane afterwards
(
0xe7fc694072e9a9a950a9db0556bfe82b0b173ecd3707893bd9a3af1388e140dd,0xc8c883117b44bf2dfafa27c1d20bb9589578368d036b0b73c5f73d5bcd9ac385).