# Mechanics of cross chain composability using Shared Publisher and Account Abstraction ## Introduction: Shared Publisher, Inboxes, and 2PC Cross-chain transactions have long been plagued by uncertainty: messages sent from one chain may or may not arrive on another, and developers often have to build complex retry or rollback logic. **Shared Publisher + 2PC** changes the game: sequencers simulate all cross-chain calls (mailbox writes), exchange XCallRequests/XCallResponses, and run a **two-phase commit** so that, at execution time, every `Inbox` slot is already pre-populated with the correct payload. This ensures **atomicity**—reading from an inbox and writing to an outbox across chains must match exactly, and the Shared Publisher verifies this via a succinct zk proof under the hood. ## Example: wETH ↔ SSV Swap via SP+xCall and AA In this example, we swap **1 wETH → SSV → wETH** across Rollup A and Rollup B—without modifying any bridge or DEX contracts. All orchestration is done off-chain in a web app that builds two AA transactions (UserOps), one per chain, each batching multiple calls. * **Chains:** A (chainId 1001) and B (chainId 1002) * **Tokens:** wETH (`0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2`) and SSV (`0x9D65fF81a3C488d585bBfb0Bfe3c7707C7917F54`) * **DEX (on B):** Uniswap V2 Router02 (`0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D`) * **Mailbox Contracts:** `0xMailboxA…`, `0xMailboxB…` * **AA Wallet:** Pre-deployed ERC‑4337 smart account (`0xAAwallet…`) on both chains ### Step 1: Initiator UserOp on Chain A ```ts // Config const chainIdA = 1001; const chainIdB = 1002; const sessionId = "sessionXYZ"; const user = "0xUser…"; const AAwalletA = "0xAAwallet…"; const bridgeA = "0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2"; // wETH const amountIn = ethers.utils.parseUnits("1.0", 18); const minOut = ethers.utils.parseUnits("0.9", 18); const deadline = Math.floor(Date.now()/1000) + 20*60; // ABIs const bridgeAbi = ["function burn(address,uint256)"]; const mailboxAbi = ["function Outbox(uint256,bytes)", "function Inbox(uint256,bytes32) view returns(bytes)"]; const aaAbi = ["function execBatch(address[] calldata,bytes[] calldata) external"]; const bridgeIF = new ethers.utils.Interface(bridgeAbi); const mailboxIF = new ethers.utils.Interface(mailboxAbi); const aaIF = new ethers.utils.Interface(aaAbi); // Helper for msgId function msgId(chain: number, session: string, step: string): string { return ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode(["uint256","string","string"], [chain, session, step]) ); } // 1) Burn on A triggers bridge to write a "mint" outbox message to B const burnCalldata = bridgeIF.encodeFunctionData("burn", [user, amountIn]); // 2) (Optional) Read return from B and mint final SSV const inboxReturn = mailboxIF.encodeFunctionData("Inbox", [chainIdB, msgId(chainIdB, sessionId, "return")]); const finalMintData = new ethers.utils.Interface(["function mint(address,uint256)"]) .encodeFunctionData("mint", [user, minOut]); // 3) Batch into execBatch const targetsA = [ bridgeA, "0xMailboxA…", bridgeA ]; const callsA = [ burnCalldata, inboxReturn, finalMintData ]; const batchA = aaIF.encodeFunctionData("execBatch", [targetsA, callsA]); const userOpA = { sender: AAwalletA, callData: batchA, // gas, signature... }; ``` > **Note:** Calling `bridgeA.burn(...)` automatically emits the outbox mint instruction (via the bridge contract’s internal hook). The sequencer pre-populates B’s inbox before you submit. ### Step 2: Executor UserOp on Chain B ```ts // Config const chainIdA = 1001; const AAwalletB = "0xAAwallet…"; const bridgeB = "0xC02aaa39b223fe8D0A0e5C4F27eAD9083C756Cc2"; // wETH const dexRouter = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"; const usdcB = "0x9D65fF81a3C488d585bBfb0Bfe3c7707C7917F54"; // SSV const amountIn = ethers.utils.parseUnits("1.0", 18); const minOut = ethers.utils.parseUnits("0.9", 18); const path = [ bridgeB, usdcB ]; const deadline = Math.floor(Date.now()/1000) + 20*60; // ABIs const mailboxAbi = ["function Inbox(uint256,bytes32) view returns(bytes)", "function Outbox(uint256,bytes) "]; const bridgeAbi = ["function mint(address,uint256)"]; const erc20Abi = ["function approve(address,uint256)"]; const dexAbi = ["function swapExactTokensForTokens(uint256,uint256,address[],address,uint256) returns(uint256[])"]; const aaAbi = ["function execBatch(address[] calldata,bytes[] calldata) external"]; const mailboxIF = new ethers.utils.Interface(mailboxAbi); const bridgeIF = new ethers.utils.Interface(bridgeAbi); const erc20IF = new ethers.utils.Interface(erc20Abi); const dexIF = new ethers.utils.Interface(dexAbi); const aaIF = new ethers.utils.Interface(aaAbi); // Helper for msgId function msgId(chain: number, session: string, step: string): string { return ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode(["uint256","string","string"], [chain, session, step]) ); } // 1) Inbox mint from A const inboxMint = mailboxIF.encodeFunctionData("Inbox", [chainIdA, msgId(chainIdA, sessionId, "mint")]); // 2) Mint bridged wETH on B const mintB = bridgeIF.encodeFunctionData("mint", [user, amountIn]); // 3) Approve & swap on Uniswap V2 const approve = erc20IF.encodeFunctionData("approve", [dexRouter, amountIn]); const swap = dexIF.encodeFunctionData("swapExactTokensForTokens", [amountIn, minOut, path, user, deadline]); // 4) Outbox return to A const returnPayload = ethers.utils.defaultAbiCoder.encode( ["string","address","address","uint256"], ["return", user, usdcB, minOut] ); const outboxReturn = mailboxIF.encodeFunctionData("Outbox", [chainIdA, returnPayload]); // 5) Batch into execBatch const targetsB = [ "0xMailboxB…", bridgeB, bridgeB, dexRouter, "0xMailboxB…" ]; const callsB = [ inboxMint, mintB, approve, swap, outboxReturn ]; const batchB = aaIF.encodeFunctionData("execBatch", [targetsB, callsB]); const userOpB = { sender: AAwalletB, callData: batchB, // gas, signature by sequencer... }; ``` **Result:** Two AA transactions (one on A, one on B) drive the full cross-chain swap, with no on-chain waiting and **no changes** to any bridge or DEX contracts. The **sequencers** handle SP+xCall simulation and 2PC to guarantee each `Inbox` call returns the right data, achieving atomic composability across chains. ## Integration Guide for dApp Developers 1. **Detect User’s Chain** The web application reads the connected wallet’s chain ID via `ethereum.request({ method: 'eth_chainId' })`. 2. **Determine Cross-Transaction** If the connected chain ID does not match the dApp’s deployed chain, the app treats the action as a cross-chain transaction and triggers the AA workflow. 3. **Build & Submit UserOps** Following the patterns above, the webapp assembles the appropriate `execBatch` UserOperations for each chain (burn/mint and swap/return), and submits them to the AA bundler or EntryPoint. ### Leading AA SDKs & Libraries * Ethers.js AA Module: First-class support for ERC-4337 with built-in UserOperation builders and EntryPoint interaction. * Account Abstraction Kit: Modular framework offering wallet templates, plugins, and helper libraries for AA across multiple EVMs. * ZeroDev SDK: High-level JavaScript/TypeScript library for sponsored AA transactions, gasless onboarding, and bundling. * Biconomy AA SDK: Intent-driven AA tooling, bundler integration, and customizable paymaster flows. * Safe SDK: Official Gnosis Safe library extended with AA batch execution and multi-signature support. * hardhat-deploy-4337: Hardhat plugin for local testing, deploying EntryPoints, and simulating AA UserOps in development. ## AA + MetaMask example Below is a minimal end-to-end example of constructing and sending an ERC-20 transfer via an ERC-4337 “UserOperation” (AA) using the Rari/Rocket Pool Account-Abstraction SDK and MetaMask as your signer/provider. It: 1. Grabs the MetaMask provider 2. Wraps it in ethers.js 3. Instantiates the Account-Abstraction helper 4. Builds and signs a UserOperation to call an ERC-20’s transfer(...) 5. Sends the UserOp off to a bundler ```ts // 1) Install: // npm install ethers @account-abstraction/sdk import { ethers } from "ethers"; import { SimpleAccountAPI, HttpRpcClient, UserOperationStruct } from "@account-abstraction/sdk"; async function sendERC20ViaAA() { // —————————————————————————————————————————————— // 1. Connect to MetaMask // —————————————————————————————————————————————— if (!window.ethereum) { throw new Error("MetaMask not detected"); } const provider = new ethers.providers.Web3Provider(window.ethereum); await provider.send("eth_requestAccounts", []); const ownerSigner = provider.getSigner(); // —————————————————————————————————————————————— // 2. Setup EntryPoint + Bundler RPC // —————————————————————————————————————————————— // Replace these with your deployed addresses const ENTRY_POINT_ADDRESS = "0xEntryPointContract…"; const ACCOUNT_FACTORY_ADDRESS = "0xAccountFactory…"; const BUNDLER_URL = "https://your-bundler.rpc.endpoint"; const rpcClient = new HttpRpcClient( BUNDLER_URL, ENTRY_POINT_ADDRESS, provider ); // —————————————————————————————————————————————— // 3. Instantiate Account API // —————————————————————————————————————————————— const accountAPI = new SimpleAccountAPI({ provider, entryPointAddress: ENTRY_POINT_ADDRESS, factoryAddress: ACCOUNT_FACTORY_ADDRESS, owner: ownerSigner }); // Compute your counterfactual AA address (for UI, optional) const aaAddress = await accountAPI.getAccountAddress(); console.log("Your AA smart-account address:", aaAddress); // —————————————————————————————————————————————— // 4. Prepare call data: ERC-20 transfer // —————————————————————————————————————————————— const ERC20 = new ethers.Contract( "0xTokenAddress…", [ "function transfer(address to, uint256 amount) public returns (bool)" ], ownerSigner ); const to = "0xRecipientAddress…"; const amount = ethers.utils.parseUnits("1.0", 18); const callData = ERC20.interface.encodeFunctionData("transfer", [ to, amount ]); // —————————————————————————————————————————————— // 5. Build, sign & send the UserOperation // —————————————————————————————————————————————— // a) createUnsignedUserOp automatically fills in nonce, gas limits, etc. let userOp: UserOperationStruct = await accountAPI.createUnsignedUserOp({ target: ERC20.address, data: callData }); // b) sign via your ownerSigner (MetaMask) userOp = await accountAPI.signUserOp(userOp); // c) dispatch to bundler const userOpHash = await rpcClient.sendUserOpToBundler(userOp); console.log("👀 userOpHash:", userOpHash); // Optionally: wait for inclusion const receipt = await rpcClient.waitForUserOpReceipt(userOpHash); console.log("✅ AA tx executed in tx:", receipt.transactionHash); } sendERC20ViaAA().catch(console.error); ```