Skip to content

feat(safe): Ethereum token-owner Safe — distinct per-chain address pin + policy-parity - #232

Merged
thedavidmeister merged 3 commits into
mainfrom
feat/rai-1109-deploy-safe-ethereum
Jul 17, 2026
Merged

feat(safe): Ethereum token-owner Safe — distinct per-chain address pin + policy-parity#232
thedavidmeister merged 3 commits into
mainfrom
feat/rai-1109-deploy-safe-ethereum

Conversation

@hardyjosh

@hardyjosh hardyjosh commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

RAI-1109 (Josh, 2026-07-07): give Ethereum the SAME token-owner Safe
address as Base rather than a fresh per-chain address.

A Safe address is CREATE2(proxyFactory, keccak(keccak(initializer) ++
saltNonce), keccak(proxyCreationCode ++ singleton)), so replaying the
exact genesis creation call on Ethereum yields the identical address.

  • LibStoxSafeGenesis pins the genesis params recovered from the Base
    creation tx (Safe tx service): the Safe was CREATED as v1.3.0 (factory
    0xa6B71E26…, singleton 0x3E5c6364…), saltNonce 0, with the verbatim
    setup initializer — 3 owners / threshold 2 — then later upgraded to
    v1.4.1 and expanded to the current 6 owners / threshold 3. Address is
    fixed by the v1.3.0 creation params, so the reproduction uses those.
    The v1.3.0 factory + singleton + fallback are already live at the same
    addresses on Ethereum (verified 2026-07-07).

  • 20260707-deploy-safe-ethereum.s.sol: EOA-broadcast
    createProxyWithNonce with the pinned genesis inputs, asserting the
    produced proxy == LibSafeInvariants.STOX_TOKEN_OWNER_SAFE (a
    genesis-param drift fails loudly), guarded against re-running once the
    target has code.

  • Fork test PROVES it: runs the real v1.3.0 factory on an Ethereum fork
    and asserts the proxy is byte-for-byte the Base Safe address, in the
    genesis 3-owner/threshold-2 state.

Scope boundary: this puts the matched ADDRESS on-chain in its genesis
state. Reaching Base's CURRENT policy (upgrade to v1.4.1, add the 3
later owners, raise threshold to 3) is a separate post-deploy replay
signed by the genesis owners — documented in the runbook § 3a.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr

Linear: RAI-1109. Sits right after the network-wiring PR (#227), below principals. Fork test proves it: the pinned genesis params reproduce the exact Base Safe address on a live Ethereum fork. Scope = the matched address in its genesis state (3 owners / threshold 2, v1.3.0); the genesis→current policy replay (upgrade to v1.4.1, +3 owners, threshold 3, signed by the genesis owners) is documented in the runbook § 3a as a follow-up. Alternative was a fresh v1.4.1 Safe with a different address.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hardyjosh, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eae0d9ef-e75e-42cf-9796-11fae718a026

📥 Commits

Reviewing files that changed from the base of the PR and between 23c37f1 and 3cd70ce.

📒 Files selected for processing (2)
  • src/lib/LibSafeInvariants.sol
  • test/src/concrete/deploy/EthereumTokenOwnerSafeParity.t.sol
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rai-1109-deploy-safe-ethereum

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hardyjosh
hardyjosh force-pushed the feat/rai-1095-ethereum-network-wiring branch from e603338 to 43cfd27 Compare July 7, 2026 11:02
@hardyjosh
hardyjosh force-pushed the feat/rai-1109-deploy-safe-ethereum branch from 199bcee to 4b188bd Compare July 7, 2026 11:03
@hardyjosh
hardyjosh force-pushed the feat/rai-1109-deploy-safe-ethereum branch from 4b188bd to 779689c Compare July 15, 2026 13:48
@hardyjosh
hardyjosh force-pushed the feat/rai-1095-ethereum-network-wiring branch from 43cfd27 to 002a564 Compare July 15, 2026 13:48
@hardyjosh
hardyjosh force-pushed the feat/rai-1109-deploy-safe-ethereum branch from 779689c to 7b93f95 Compare July 15, 2026 15:04
@hardyjosh hardyjosh changed the title feat(script): reproduce the token-owner Safe at its Base address on Ethereum feat(safe): Ethereum token-owner Safe — distinct per-chain address pin + policy-parity Jul 15, 2026
Josh Hardy and others added 3 commits July 17, 2026 08:54
…thereum

RAI-1109 (Josh, 2026-07-07): give Ethereum the SAME token-owner Safe
address as Base rather than a fresh per-chain address.

A Safe address is CREATE2(proxyFactory, keccak(keccak(initializer) ++
saltNonce), keccak(proxyCreationCode ++ singleton)), so replaying the
exact genesis creation call on Ethereum yields the identical address.

- LibStoxSafeGenesis pins the genesis params recovered from the Base
  creation tx (Safe tx service): the Safe was CREATED as v1.3.0 (factory
  0xa6B71E26…, singleton 0x3E5c6364…), saltNonce 0, with the verbatim
  setup initializer — 3 owners / threshold 2 — then later upgraded to
  v1.4.1 and expanded to the current 6 owners / threshold 3. Address is
  fixed by the v1.3.0 creation params, so the reproduction uses those.
  The v1.3.0 factory + singleton + fallback are already live at the same
  addresses on Ethereum (verified 2026-07-07).

- 20260707-deploy-safe-ethereum.s.sol: EOA-broadcast
  createProxyWithNonce with the pinned genesis inputs, asserting the
  produced proxy == LibSafeInvariants.STOX_TOKEN_OWNER_SAFE (a
  genesis-param drift fails loudly), guarded against re-running once the
  target has code.

- Fork test PROVES it: runs the real v1.3.0 factory on an Ethereum fork
  and asserts the proxy is byte-for-byte the Base Safe address, in the
  genesis 3-owner/threshold-2 state.

Scope boundary: this puts the matched ADDRESS on-chain in its genesis
state. Reaching Base's CURRENT policy (upgrade to v1.4.1, add the 3
later owners, raise threshold to 3) is a separate post-deploy replay
signed by the genesis owners — documented in the runbook § 3a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
… reproduction

Abandon the matched/deterministic Safe address (Josh, 2026-07-15). Each chain
gets its OWN ST0x owner Safe at a distinct address — deployed out-of-band as a
clean v1.4.1 Safe with Base's owner set + threshold + policy, then pinned. The
address is a per-chain deploy artifact (like the clone + token addresses), NOT
a principal; the whole POLICY is the shared pin set.

- Delete the genesis-replay machinery: `20260707-deploy-safe-ethereum.s.sol`
  and `LibStoxSafeGenesis`. We deploy the Safe ourselves; no in-repo script.
- `LibSafeInvariants`: add `STOX_TOKEN_OWNER_SAFE_ETHEREUM` (placeholder),
  `safeForChainId(block.chainid)`, an order-INSENSITIVE `assertOwnerSetUnordered`
  (a fresh Safe's getOwners() order is incidental — not a policy property), and
  `assertPolicyMatchesBase` = immutable v1.4.1 identity + owner SET + threshold.
- Replace the deploy test with `EthereumTokenOwnerSafeParityTest`: placeholder-
  gated (PENDING until pinned), then asserts the live Ethereum Safe matches
  Base's policy in every way that matters, and is a distinct address. Runs in
  CI, so it stays red into the future on any drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
…leton

Pin STOX_TOKEN_OWNER_SAFE_ETHEREUM to the deployed Safe
(0x3840aeDaEc8e82f79d8F6a8F6ADCa271E13E0329) and make the Safe identity check
L1/L2-tolerant: Ethereum mainnet runs the v1.4.1 L1 `Safe` singleton (the Safe
UI default on mainnet) while Base runs the L2 `SafeL2`. assertImmutableInvariants
now accepts either known SafeProxy codehash and either canonical v1.4.1
singleton (+ its codehash), keeping owners/threshold/version/modules/guard/
fallback as the shared policy. Proxy codehash stays the first (raw) check so
an EOA/fake proxy is still rejected before any call.

Verified on live forks: the Ethereum Safe matches Base's policy in every way
EXCEPT threshold (still 1-of-6; being raised to 3-of-6 on-chain) — so the
parity test is red on exactly that until the changeThreshold lands. Base's
L2 path unchanged (LibSafeInvariantsTest 12/12).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
@hardyjosh
hardyjosh changed the base branch from feat/rai-1095-ethereum-network-wiring to graphite-base/232 July 17, 2026 09:05
@hardyjosh
hardyjosh force-pushed the feat/rai-1109-deploy-safe-ethereum branch from 855d150 to 3cd70ce Compare July 17, 2026 09:05
@hardyjosh
hardyjosh changed the base branch from graphite-base/232 to main July 17, 2026 09:05
@hardyjosh
hardyjosh marked this pull request as ready for review July 17, 2026 14:48
@thedavidmeister
thedavidmeister merged commit 6a9d018 into main Jul 17, 2026
10 of 15 checks passed
thedavidmeister pushed a commit that referenced this pull request Jul 20, 2026
…ddress(0)

The Ethereum Safe is pinned (#232), so the token-deploy pre-flight reverts
through assertPolicyMatchesBase's proxy-codehash check when the pinned Safe
isn't live on the active chain (no code with no fork), rather than through the
address(0) EthereumSafeNotReady guard. Update the authorize/verify inverted
tests to expect SafeProxyCodehashMismatch. Stable regardless of the Safe's
on-chain threshold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
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