Skip to content

feat: Shadow Accounts — cross-chain smart account presence via interop#2112

Open
valera-grinenko-ai wants to merge 9 commits intomatter-labs:draft-v32from
valera-grinenko-ai:shadow-accounts
Open

feat: Shadow Accounts — cross-chain smart account presence via interop#2112
valera-grinenko-ai wants to merge 9 commits intomatter-labs:draft-v32from
valera-grinenko-ai:shadow-accounts

Conversation

@valera-grinenko-ai
Copy link
Copy Markdown

@valera-grinenko-ai valera-grinenko-ai commented Apr 3, 2026

Summary

Implements Shadow Accounts as a standalone protocol building block on draft-v32. A Shadow Account is a smart account deployed on a remote chain via interop, giving a home-chain user contract interaction capabilities there without an EOA or separate deployment.

Primary use cases (from spec):

  • Origin-routed private interop (B→A→C): ShadowAccount on the origin chain automates the A→C relay leg within a single user transaction on B.
  • L2→L1 interop: ShadowAccount on L1 lets an L2 user interact with L1 contracts (governance, DeFi) without a separate L1 account.

What this PR adds

New contracts (l1-contracts/contracts/interop/):

  • ShadowAccount.sol — ERC-7786 recipient with two-layer authorization (InteropHandler as caller, ERC-7930 owner as sender). Executes ShadowAccountCall[] payloads supporting both call and delegatecall (for script contracts that read remote state). Caches keccak256(owner) at initialization for gas-efficient authorization.
  • ShadowAccountFactory.sol — CREATE2 deployer. One account per owner per chain. Salt = keccak256(ERC-7930 encoded owner) ensures deterministic addresses without querying the target chain. predictAddress is public for gas-efficient internal use.
  • IShadowAccount.sol, IShadowAccountFactory.sol — Interfaces and data types.

Modified contracts:

  • InteropHandler._executeCalls — When interopCall.shadowAccount == true, routes through the sender's ShadowAccount (lazy-deploying via factory) instead of calling interopCall.to directly.
  • InteropCenter — Parses new shadowAccount(bool) ERC-7786 call attribute; enforces mutual exclusion with indirectCall; propagates flag to InteropCall.
  • IERC7786Attributes — Added shadowAccount(bool) attribute declaration.
  • CallAttributes struct — Added bool shadowAccount field (packed with indirectCall for storage efficiency).
  • Config.solSUPPORTED_INTEROP_ATTRIBUTES 5 → 6.
  • L2ContractAddresses.solL2_SHADOW_ACCOUNT_FACTORY_ADDR at 0x10012.
  • L2ContractInterfaces.solL2_SHADOW_ACCOUNT_FACTORY interface constant.
  • InteropErrors.sol — Six new shadow account error types (with selectors via errors-lint).

Design decisions

Decision Choice Rationale
Factory address Built-in 0x12 Next slot after L2_BASE_TOKEN_HOLDER (0x11); follows existing pattern
CREATE2 salt keccak256(ERC-7930 owner) Encodes both chain ID and address → one account per owner per chain
No constructors/immutables Initialization pattern Required by zksync OS (per AGENTS.md)
Owner hash caching _ownerHash = keccak256(owner) at init Avoids re-hashing variable-length bytes on every receiveMessage call
shadowAccount + indirectCall Mutually exclusive Fundamentally different routing modes; combining has no defined semantics
interopCall.to when shadow=true Set by user, ignored by InteropHandler Handler overrides routing; to preserved for event logging

What this PR does NOT include

  • BZZK / private interop implementation (follow-up PR).
  • Speculative hooks or unrelated refactors.
  • Changes to GWAssetTracker, L2AssetRouter, or L2AssetTracker.

Test plan

23 new tests in L2ShadowAccountTestAbstract.t.sol / L2ShadowAccountL1Test.t.sol:

  • Factory (5): deterministic deployment, idempotency, different-owner / different-chain addressing, event emission
  • Initialization (3): owner storage, event emission, double-init revert, factory-only access
  • Authorization (3): InteropHandler-only caller, owner-only sender, happy path
  • Call execution (6): value transfers, multi-call sequencing, event emission, failure revert, delegatecall, empty payload edge case
  • ETH reception (1): receive() accepts ETH
  • InteropHandler integration (2): end-to-end shadow account routing via bundle, non-shadow regression
  • Fuzz (2): address uniqueness across owners (256 runs), predict-matches-deploy invariant (256 runs)
  • Regression: all 164 tests pass across 20 suites (0 failures, 0 regressions)

🤖 Generated with Claude Code

Shadow Accounts give a home-chain user a smart account on remote chains,
enabling automated relay during bundle execution (e.g., origin-routed
private interop B→A→C) and L2→L1 contract interaction without a
separate L1 account.

Core design:
- ShadowAccount implements IERC7786Recipient with two-layer auth:
  msg.sender must be InteropHandler, ERC-7786 sender must match owner.
- ShadowAccountFactory deploys one account per owner per chain via
  CREATE2 (salt = keccak256(ERC-7930 encoded owner)).
- InteropHandler routes calls through shadow accounts when
  InteropCall.shadowAccount is true (lazy deploy via factory).
- New shadowAccount(bool) ERC-7786 call attribute, mutually exclusive
  with indirectCall.
- Supports arbitrary calls and delegatecalls (for script contracts).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shadow Accounts and others added 2 commits April 3, 2026 22:35
- Reorder CallAttributes fields for efficient 32-byte slot packing
  (bools grouped together) to satisfy gas-struct-packing lint rule.
- Regenerate selectors and zkstack-out (InteropCenter ABI).
- Exclude delegatecall-loop from Slither — ShadowAccount delegatecall
  targets are chosen by the authenticated owner, not by untrusted input.
- Prettier formatting fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ran recompute_hashes.sh with the expected compiler version
(foundry-zksync-v0.1.5, commit 807f47ace) to regenerate
AllContractsHashes.json and zkstack-out artifacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shadow Accounts and others added 6 commits April 4, 2026 08:06
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…used helper

- Update InteropCall.shadowAccount NatSpec to reflect implementation
  (was still saying "always false, not yet implemented").
- Simplify shadow account routing in _executeCalls to a ternary.
- Remove unused _buildSingleDelegatecallPayload test helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cache keccak256(owner) as _ownerHash in ShadowAccount to avoid
  re-hashing variable-length bytes on every receiveMessage call.
- Change predictAddress from external to public so the factory can
  call it internally without an external self-call (this.predictAddress).
- Remove assert in factory — rely on CREATE2 determinism directly.
- Recompute AllContractsHashes.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0xValera review pattern: demands edge-case coverage and "checking of
effects of interactions, not just calling functions."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop redundant vm.mockCall on call targets — etched STOP opcode
  already makes the call succeed cleanly.
- Replace vm.mockCallRevert with a real AlwaysReverter contract.
- Remove vm.mockCall on the predicted shadow account in the
  InteropHandler integration test — the real CREATE2-deployed
  ShadowAccount now runs receiveMessage end-to-end. Added assertions
  verifying the account is deployed and its owner matches the sender.

The L2_MESSAGE_VERIFICATION mock is kept (matches every other interop
test in the file — cannot remove without a real L2→L1 Merkle proof).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kelemeno
Copy link
Copy Markdown
Contributor

kelemeno commented Apr 17, 2026

please point this at #2076 it is a modification of that branch. It also has some other work, which ideally does not get lost

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants