From 6cfd8b89aa614b5edb52efd06f703159a2c064d7 Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Sun, 19 Oct 2025 12:17:25 +0300 Subject: [PATCH 01/10] universal bridge with comment fixes --- universal_bridge.md | 376 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 universal_bridge.md diff --git a/universal_bridge.md b/universal_bridge.md new file mode 100644 index 0000000..deacc53 --- /dev/null +++ b/universal_bridge.md @@ -0,0 +1,376 @@ + +# Universal Shared Bridge for OP Chains +*(Validity-Proof / OP-Succinct Compatible; Direct L2↔L2 “mint-on-message” fast path)* + +## Objectives & Authoritative Requirements + +1. **L1 Custody:** Ethereum (L1) is the main escrow (but not the only one) for **canonical ERC-20s and ETH**. +2. **L1→L2 Representation:** Depositing an L1 ERC-20 to any OP L2 mints a **ComposeableERC20 (CET)** on that L2. +3. **L2↔L2 Bridging:** The bridge supports moving both **native ERC-20** and **CET** between OP L2s. +4. **L2↔L2 (ERC-20):** If the source asset is a **native ERC-20** on L2, it is **Locked** on the source L2 and a **CET is Minted** on the destination L2. +5. **L2↔L2 (CET):** If the source asset is a **CET**, it is **Burned** on the source L2 and **Minted** on the destination L2. +6. **L2→L1 Redemption:** Any CET can always be redeemed to **unlock the collateral on L1**. +7. **Proof System:** The design **builds on OP-Succinct** (validity proofs). L1 verifications rely on **per-L2 finalized post roots** +8. **Mint Authority / Safety:** Only **bridge contracts** may mint or burn **CET**; no external mint paths. +9. **Exit Logic (proof-based paths):** Use the **same exit logic as OP-Succinct**: prove claims against a chain’s **post root**, then an **MPT/storage proof** to the chain’s **Outbox/Exit root**, then a **Merkle inclusion** for the exit record. +10. **Replay Protection:** Messages can be consumed only once. Any replay will be ignored. +11. **Inter-L2 Fast Path:** For **L2↔L2** transfers, the destination **mints on receipt of a bridge message** (no proof verification at claim time). **Later settlement** is done simultaneously via aggregated proofs (out of scope here). +12. **TBD** Allow token owner to have the bridge mint native token on specified conditions. + +--- + +## Introducing the ComposeableERC20 + +Inspired by `OptimismSuperChainERC20`, this is an ERC7802 compliant token for cross-chain transfers. +The code snippet below describe the main functionality. + +```solidity +abstract contract ComposeableERC20 is ERC20, IERC7802 { + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external { + if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); + + _mint(_to, _amount); + + emit CrosschainMint(_to, _amount, msg.sender); + } + + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external { + if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); + + _burn(_from, _amount); + + emit CrosschainBurn(_from, _amount, msg.sender); + } + + /// @notice Storage struct for the BridgedComposeTokenERC20 metadata. + struct BridgedComposeTokenERC20Metadata { + /// @notice The ChainID where this token was originally minted. + uint256 remoteChainID + /// @notice Address of the corresponding version of this token on the remote chain. + address remoteToken; + /// @notice Name of the token + string name; + /// @notice Symbol of the token + string symbol; + /// @notice Decimals of the token + uint8 decimals; + } +``` + +___ + +# L2↔L2 Bridge --- Sessioned Mailbox Flow + + +### Entities & Contracts + +- **Bridge (per L2)** + Handles: locking native ERC-20, burning CET, mailbox write/read, and + minting (via token's `onlyBridge` gate when called from the bridge + context). + +- **Mailbox (per L2)** + Minimal append-only message bus with read-once semantics per + `(chainSrc, recipient, sessionId, label)`. + + - `write(chainId, account, sessionId, label, payload)`\ + - `read(chainId, account, sessionId, label) → bytes` + (consumes/marks delivered) + +- **Token types** + + - **Native ERC-20** on an L2 (not minted by bridge). + - **CET** (ComposeableERC20Token) --- canonical L2 representation + of L1/L2 asset. + - `mint`/`burn` are **restricted** to `msg.sender == Bridge`. + +- **Deterministic CET Addresses (Superchain-style)** + Each CET contract is deployed **at the same address across all OP + L2s**, deterministically derived from the L1 canonical asset address + (using CREATE2/CREATE3). + + - This eliminates the need for a per-chain registry. + - Mailbox payloads only need to carry the **`remoteToken` address**. + - On the destination chain, the bridge deterministically computes + the CET address and mints to the receiver. + +------------------------------------------------------------------------ + +### Message Schema (Mailbox payload) + +All `SEND` payloads use a single canonical ABI encoding: + + abi.encode( + address sender, // original EOA or contract on source L2 + address receiver, // recipient on destination L2 + address remoteAsset, // canonical L1 asset address + uint256 amount // amount (decimals same as its CET) + ) + +Notes: - On **destination L2**, the bridge computes the **CET address +deterministically** from `remoteAsset` +- This ensures the CET address is consistent across all L2s, without a +registry. + +ACK payload: + + abi.encode("OK") + +Labels: - Outbound: `"SEND"` - Return ACK: `"ACK SEND"` + +------------------------------------------------------------------------ + + +### Source L2: bridge entrypoints (sessionized) + + +``` solidity +/// Lock native ERC-20 on source and send SEND message +function bridgeERC20To( + uint256 chainDest, // Destination ChainID + address tokenSrc, // native ERC-20 on source L2 + uint256 amount, + address receiver, // address on destination L2 + uint256 sessionId +) external; + +/// Burn CET on source and send SEND message +function bridgeCETTo( + uint256 chainDest, // Destination ChainID + address cetTokenSrc, // CET on source L2 + uint256 amount, + address receiver, // address on destination L2 + uint256 sessionId +) external; +``` + +### Destination L2: recipient claim + +``` solidity +/// Process funds reception on the destination chain +/// @param chainSrc source chain identifier the funds are sent from +/// @param chainDest dest chain identifier the funds are sent to +/// @param sender address of the sender of the funds +/// @param receiver address of the receiver of the funds +/// @param sessionId identifier of the user session +/// @return token address of the token that was transferred +/// @return amount amount of tokens transferred +function receiveTokens( + uint256 chainSrc, + uint256 chainDest, + address sender, + address receiver, + uint256 sessionId +) external returns (address token, uint256 amount); +``` + +> Note: receiveTokens` sits **on the Bridge contract** +> (so that when it calls `CrossChainMint`, the token sees +> `msg.sender == Bridge` and respects the `onlyBridge` mint gate), while +> still requiring `msg.sender == receiver` to enforce "only receiver can +> claim". + +------------------------------------------------------------------------ + +### Events + +``` solidity +event TokensSendQueued( + uint256 indexed chainDest, + address indexed sender, + address indexed receiver, + address remoteAsset, + uint256 amount, + uint256 sessionId, + bytes32 messageId +); + +event TokensLocked(address indexed token, address indexed sender, uint256 amount); +event CETBurned(address indexed token, address indexed sender, uint256 amount); +event MailboxWrite(uint256 indexed chainId, address indexed account, uint256 indexed sessionId, string label); +event TokensReceived(address indexed token, uint256 amount); +event MailboxAckWrite(uint256 indexed chainId, address indexed account, uint256 indexed sessionId, string label); +``` + +------------------------------------------------------------------------ + +### Source L2 --- Execution Flow & Pseudocode +### `bridgeERC20To` + +``` solidity +function bridgeERC20To( + uint256 chainDest, + address tokenSrc, + uint256 amount, + address receiver, + uint256 sessionId +) external { + address sender = msg.sender; + + IERC20(tokenSrc).transferFrom(sender, address(this), amount); + emit TokensLocked(tokenSrc, sender, amount); + + address remoteAsset = ERC20Metadata(tokenSrc).remoteSource(); // canonical L1 address + + bytes memory payload = abi.encode(sender, receiver, remoteAsset, amount); + + mailbox.write(chainDest, receiver, sessionId, "SEND", payload); + // performs a mailbox read for an "ACK" labeled message. + checkAck(chainDest, sessionID) + + emit MailboxWrite(chainDest, receiver, sessionId, "SEND"); + + bytes32 messageId = keccak256(abi.encodePacked(chainDest, receiver, sessionId, "SEND")); + emit TokensSendQueued(chainDest, sender, receiver, remoteAsset, amount, sessionId, messageId); +} +``` + +### `bridgeCETTo` + +``` solidity +function bridgeCETTo( + uint256 chainDest, + address cetTokenSrc, + uint256 amount, + address receiver, + uint256 sessionId +) external { + address sender = msg.sender; + + ICET(cetTokenSrc).crossChainburn(sender, amount); + emit CETBurned(cetTokenSrc, sender, amount); + + address remoteAsset = ICET(cetTokenSrc).remoteSource(); // extract canonical L1 address + + bytes memory payload = abi.encode(sender, receiver, remoteAsset, amount); + + mailbox.write(chainDest, receiver, sessionId, "SEND", payload); + checkAck(chainDest, sessionID) + + emit MailboxWrite(chainDest, receiver, sessionId, "SEND"); + + bytes32 messageId = keccak256(abi.encodePacked(chainDest, receiver, sessionId, "SEND")); + emit TokensSendQueued(chainDest, sender, receiver, remoteAsset, amount, sessionId, messageId); +} +``` + +------------------------------------------------------------------------ + +### Destination L2 --- Claim Flow +``` solidity +function receiveTokens( + uint256 chainSrc, + uint256 chainDest, + address sender, + address receiver, + uint256 sessionId +) external returns (address token, uint256 amount) { + require(msg.sender == receiver, "Only receiver can claim"); + require(chainDest == block.chainid, "Wrong destination chain"); + + // 1) Read and consume the SEND message + bytes memory m = mailbox.read(chainSrc, receiver, sessionId, "SEND"); + if (m.length == 0) revert("No SEND message"); + + address readSender; + address readReceiver; + address remoteAsset; + + (readSender, readReceiver, remoteAsset, amount) = + abi.decode(m, (address, address, address, uint256)); + + require(readSender == sender, "Sender mismatch"); + require(readReceiver == receiver, "Receiver mismatch"); + + // 2) RELEASE if native token is hosted & escrowed here, else MINT BCT + address native = nativeTokenOnThisChain(remoteAsset); + if (native != address(0) && IERC20(native).balanceOf(address(this)) >= amount) { + // Release escrowed native tokens + require(IERC20(native).transfer(receiver, amount), "Native release failed"); + token = native; + } else { + // Mint deterministic BCT on this chain + token = computeBCTAddress(remoteAsset); + ICET(token).crossChainMint(receiver, amount); + } + + // 3) ACK back to source + mailbox.write(chainSrc, sender, sessionId, "ACK SEND", abi.encode("OK")); + + emit TokensReceived(token, amount); + return (token, amount); +} +``` + +------------------------------------------------------------------------ + +### Safety & Replay + +- **Mint authority:** CET enforces `onlyBridge` on `mint`/`burn`. +- **Replay protection:** `mailbox.read(...)` must be consume-once. +- **No registry needed:** CET address is computed deterministically + from L1 asset address. +- **ACK:** Ensures mailbox equivalency. + +------------------------------------------------------------------------ +### Mailbox Interface + +``` solidity +interface IMailbox { + function write( + uint256 chainId, + address account, + uint256 sessionId, + string calldata label, + bytes calldata payload + ) external; + + function read( + uint256 chainId, + address account, + uint256 sessionId, + string calldata label + ) external returns (bytes memory); +} +``` + +------------------------------------------------------------------------ + +### End-to-End Lifecycle + +1. Sender calls `bridgeERC20To` or `bridgeCETTo` with sessionId. +2. Source bridge locks/burns + writes `"SEND"` with L1 asset address. +3. Receiver calls `receiveTokens`. +4. Destination bridge reads `"SEND"`, computes CET address from + `remoteAsset`, mints, writes `"ACK SEND"` +5. ACK is visible for source monitoring. + +------------------------------------------------------------------------ + + + +## Modify L1 contracts to make Shared Deposits work + + + + + +### OpPortal2.sol + +1. Needs to work with CET standard +2. Need to have a `LockBox` that supports ERC20s. Must always use the lockbox. + + +**Note regarding interop sequencers** + +Currently Op-Succinct Sequencers pick up `TransactionDeposited` Events to relay bridge messages. +They check the address of the contract that originated the event. And perform a ZK proof that the event was included in the `recieptsRoot` + +In the case of an external interop sequencer, a malicious wrapped sequencer may send a non backed log. This won't be ZK proven but it will still become part of the external rollup canonical state. +The result will be that the connection with the Universal Shared Bridge will be severed. + + From 3fe3e173bffe04f4a0c3ab71da11b84054081c2e Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Sun, 19 Oct 2025 22:14:39 +0300 Subject: [PATCH 02/10] fix mailbox --- universal_bridge.md | 122 ++++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index deacc53..215b5fe 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -1,6 +1,7 @@ # Universal Shared Bridge for OP Chains -*(Validity-Proof / OP-Succinct Compatible; Direct L2↔L2 “mint-on-message” fast path)* + +The Universal Shared Bridge for OP Chains enables seamless asset transfers between Ethereum (L1) and any supporting OP L2, as well as directly between OP L2s. It leverages canonical custody on any chain, minting of "ComposeableERC20" (CET) tokens to represent bridged assets on L2s, and supports secure burn/mint logic for moving assets between L2s. All mint/burn operations are exclusively handled by the bridge contracts, ensuring safety. The bridge employs OP-Succinct proof mechanisms for L1 verifications and introduces efficient message-based transfers for L2↔L2 bridging, with formal post-transfer settlement. ## Objectives & Authoritative Requirements @@ -81,7 +82,7 @@ ___ - `read(chainId, account, sessionId, label) → bytes` (consumes/marks delivered) -- **Token types** +- **Supported Token types** - **Native ERC-20** on an L2 (not minted by bridge). - **CET** (ComposeableERC20Token) --- canonical L2 representation @@ -98,35 +99,91 @@ ___ - On the destination chain, the bridge deterministically computes the CET address and mints to the receiver. + **TODO** More details? + ------------------------------------------------------------------------ -### Message Schema (Mailbox payload) +### Mailbox + +Mailbox is a container of `Messages` divided into 2 boxes: +- `Inbox` that has messages consumed by `Read()` function +- `Outbox` that has messages pushed into by `Write()` function. + + +``` solidity + struct Message { + MessageHeader header, + bytes payload + } + +// Header for message. Its hash can serve as the message Identifier. + struct MessageHeader { + uint256 sessionId; // 16 bytes of version + 240 bytes of value chosen by the user + uint256 chainSrc; // chain ID where the message was sourced from + uint256 chainDest; // chain ID of target destination + address sender; // The address on `chainSrc` initiating the message + address receiver; // The address on `chainDest` receiving the message + string label; // Helps decipher the payload. + } + +interface IMailbox { + // `sender` writes to the OUTBOX a message to be relayed to `reciever` on `chainDest` + function write( + Message calldata message + ) external; + + // `receiver` reads from the INBOX a message relayed by `sender` from `srcChain` + function read( + MessageHeader calldata header + ) external returns (bytes memory); +} +``` + + + +#### SessionID + +SessionID is a 240 bits random value that MUST NOT be reused across MessageHeaders with otherwise similar values. +However, every message in a cross-chain exchange mapping to a single atomic operation must carry the same SessionID. + +The first 16 bytes serve as version. Currently the only canonical version is 0. + +#### Replay Protection + +The `Read` function will return an error if it will be invoked more than once with the same `MessageHeader` + + +------------------------------------------------------------------------ + + +### Payload Schema + +The bridge supports 2 payload types that have the following labels on the *Mailbox*: + +- `SEND` +- `ACK` + +#### SEND Payload All `SEND` payloads use a single canonical ABI encoding: abi.encode( - address sender, // original EOA or contract on source L2 - address receiver, // recipient on destination L2 - address remoteAsset, // canonical L1 asset address - uint256 amount // amount (decimals same as its CET) + uint256 remoteChainID // The native chain of the transferred asset + address remoteAsset, // canonical asset address + uint256 amount // amount ) -Notes: - On **destination L2**, the bridge computes the **CET address -deterministically** from `remoteAsset` -- This ensures the CET address is consistent across all L2s, without a -registry. -ACK payload: +#### ACK payload: abi.encode("OK") -Labels: - Outbound: `"SEND"` - Return ACK: `"ACK SEND"` - ------------------------------------------------------------------------ -### Source L2: bridge entrypoints (sessionized) +### Source L2: bridge entrypoints +It is important to note that ``` solidity /// Lock native ERC-20 on source and send SEND message @@ -140,10 +197,11 @@ function bridgeERC20To( /// Burn CET on source and send SEND message function bridgeCETTo( - uint256 chainDest, // Destination ChainID - address cetTokenSrc, // CET on source L2 + uint256 chainDest, // Destination ChainID + address cetTokenSrc, // CET on source L2 + address remoteAssetAddress // original uint256 amount, - address receiver, // address on destination L2 + address receiver, // address on destination L2 uint256 sessionId ) external; ``` @@ -214,9 +272,7 @@ function bridgeERC20To( IERC20(tokenSrc).transferFrom(sender, address(this), amount); emit TokensLocked(tokenSrc, sender, amount); - address remoteAsset = ERC20Metadata(tokenSrc).remoteSource(); // canonical L1 address - - bytes memory payload = abi.encode(sender, receiver, remoteAsset, amount); + bytes memory payload = abi.encode(sender, receiver, tokenSrc, amount); mailbox.write(chainDest, receiver, sessionId, "SEND", payload); // performs a mailbox read for an "ACK" labeled message. @@ -225,7 +281,7 @@ function bridgeERC20To( emit MailboxWrite(chainDest, receiver, sessionId, "SEND"); bytes32 messageId = keccak256(abi.encodePacked(chainDest, receiver, sessionId, "SEND")); - emit TokensSendQueued(chainDest, sender, receiver, remoteAsset, amount, sessionId, messageId); + emit TokensSendQueued(chainDest, sender, receiver, tokenSrc, amount, sessionId, messageId); } ``` @@ -316,28 +372,6 @@ function receiveTokens( from L1 asset address. - **ACK:** Ensures mailbox equivalency. ------------------------------------------------------------------------- -### Mailbox Interface - -``` solidity -interface IMailbox { - function write( - uint256 chainId, - address account, - uint256 sessionId, - string calldata label, - bytes calldata payload - ) external; - - function read( - uint256 chainId, - address account, - uint256 sessionId, - string calldata label - ) external returns (bytes memory); -} -``` - ------------------------------------------------------------------------ ### End-to-End Lifecycle From 077deaf1c2e8e7c39b9a56d18f2d0bdc89d30c58 Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Sun, 19 Oct 2025 22:30:48 +0300 Subject: [PATCH 03/10] more fixes to bridge --- universal_bridge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/universal_bridge.md b/universal_bridge.md index 215b5fe..d20b9cc 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -302,7 +302,7 @@ function bridgeCETTo( address remoteAsset = ICET(cetTokenSrc).remoteSource(); // extract canonical L1 address - bytes memory payload = abi.encode(sender, receiver, remoteAsset, amount); + bytes memory payload = abi.encode(remoteAsset, amount); mailbox.write(chainDest, receiver, sessionId, "SEND", payload); checkAck(chainDest, sessionID) From c9643fa8cef81e3b2e4867af7c48dfb3ebeb5416 Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Mon, 20 Oct 2025 16:09:33 +0300 Subject: [PATCH 04/10] L1->L2 --- universal_bridge.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index d20b9cc..38111ff 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -385,21 +385,17 @@ function receiveTokens( ------------------------------------------------------------------------ +## L1 <-> L2 Bridge For native rollups. +Currently an OP rollup manage the L1<->L2 bridge via `OptimismPortal2` contract. This utilizes an `ETHLockbox` contract that locks all deposited ETH. Each native rollups using the universal bridge will deploy a `ComposePortal` (similar to `OptimismPortal) that will use a shared `ETHLockBox` and an `ERC20LockBox`. The sharing of a single `LockBox` will ensure that funds deposited on any chain can be withdrawn via another chain. -## Modify L1 contracts to make Shared Deposits work +## TODO: Can we do L1<->L2 bridge for external rollups +### ETH +Not w.o having liquidity available on the external rollup. - - -### OpPortal2.sol - -1. Needs to work with CET standard -2. Need to have a `LockBox` that supports ERC20s. Must always use the lockbox. - - -**Note regarding interop sequencers** +### ERC-20 Currently Op-Succinct Sequencers pick up `TransactionDeposited` Events to relay bridge messages. They check the address of the contract that originated the event. And perform a ZK proof that the event was included in the `recieptsRoot` From b6373f9b8fc0f7ccdce6806e934f7f940bee96a7 Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Mon, 20 Oct 2025 17:08:14 +0300 Subject: [PATCH 05/10] fix send to use chain ID --- universal_bridge.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index 38111ff..8113af4 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -16,7 +16,7 @@ The Universal Shared Bridge for OP Chains enables seamless asset transfers betwe 9. **Exit Logic (proof-based paths):** Use the **same exit logic as OP-Succinct**: prove claims against a chain’s **post root**, then an **MPT/storage proof** to the chain’s **Outbox/Exit root**, then a **Merkle inclusion** for the exit record. 10. **Replay Protection:** Messages can be consumed only once. Any replay will be ignored. 11. **Inter-L2 Fast Path:** For **L2↔L2** transfers, the destination **mints on receipt of a bridge message** (no proof verification at claim time). **Later settlement** is done simultaneously via aggregated proofs (out of scope here). -12. **TBD** Allow token owner to have the bridge mint native token on specified conditions. +12. **TODO:** Allow token owner to have the bridge mint native token on specified conditions. --- @@ -52,7 +52,7 @@ abstract contract ComposeableERC20 is ERC20, IERC7802 { /// @notice The ChainID where this token was originally minted. uint256 remoteChainID /// @notice Address of the corresponding version of this token on the remote chain. - address remoteToken; + address remoteAsset; /// @notice Name of the token string name; /// @notice Symbol of the token @@ -260,6 +260,7 @@ event MailboxAckWrite(uint256 indexed chainId, address indexed account, uint256 ### `bridgeERC20To` ``` solidity +// bridges all non CET ERC-20 tokens function bridgeERC20To( uint256 chainDest, address tokenSrc, @@ -272,7 +273,7 @@ function bridgeERC20To( IERC20(tokenSrc).transferFrom(sender, address(this), amount); emit TokensLocked(tokenSrc, sender, amount); - bytes memory payload = abi.encode(sender, receiver, tokenSrc, amount); + bytes memory payload = abi.encode(block.chainid, tokenSrc, amount); mailbox.write(chainDest, receiver, sessionId, "SEND", payload); // performs a mailbox read for an "ACK" labeled message. @@ -300,9 +301,11 @@ function bridgeCETTo( ICET(cetTokenSrc).crossChainburn(sender, amount); emit CETBurned(cetTokenSrc, sender, amount); - address remoteAsset = ICET(cetTokenSrc).remoteSource(); // extract canonical L1 address + address remoteAsset = ICET(cetTokenSrc).remoteAsset(); + uint256 remoteChainID = ICET(cetTokenSrc).remoteChainID(); - bytes memory payload = abi.encode(remoteAsset, amount); + + bytes memory payload = abi.encode(remoteChainID, remoteAsset, amount); mailbox.write(chainDest, receiver, sessionId, "SEND", payload); checkAck(chainDest, sessionID) @@ -310,7 +313,7 @@ function bridgeCETTo( emit MailboxWrite(chainDest, receiver, sessionId, "SEND"); bytes32 messageId = keccak256(abi.encodePacked(chainDest, receiver, sessionId, "SEND")); - emit TokensSendQueued(chainDest, sender, receiver, remoteAsset, amount, sessionId, messageId); + emit TokensSendQueued(chainDest, sender, receiver, cetTokenSrc, amount, sessionId, messageId); } ``` @@ -319,32 +322,28 @@ function bridgeCETTo( ### Destination L2 --- Claim Flow ``` solidity function receiveTokens( - uint256 chainSrc, - uint256 chainDest, - address sender, - address receiver, - uint256 sessionId + MessageHeader msgHeader ) external returns (address token, uint256 amount) { - require(msg.sender == receiver, "Only receiver can claim"); - require(chainDest == block.chainid, "Wrong destination chain"); + require(msg.sender == msgHeader.receiver, "Only receiver can claim"); + require(msgHeader.chainDest == block.chainid, "Wrong destination chain"); + require(msgHeader.label == "SEND", "Must read a SEND message") // 1) Read and consume the SEND message - bytes memory m = mailbox.read(chainSrc, receiver, sessionId, "SEND"); + bytes memory m = mailbox.read(MessageHeader(sessionIDchainSrc, receiver, sessionId, "SEND"); if (m.length == 0) revert("No SEND message"); address readSender; address readReceiver; address remoteAsset; - (readSender, readReceiver, remoteAsset, amount) = - abi.decode(m, (address, address, address, uint256)); + (remoteChainID, remoteAsset, amount) = + abi.decode(m, (uint256, address, uint256)); require(readSender == sender, "Sender mismatch"); require(readReceiver == receiver, "Receiver mismatch"); // 2) RELEASE if native token is hosted & escrowed here, else MINT BCT - address native = nativeTokenOnThisChain(remoteAsset); - if (native != address(0) && IERC20(native).balanceOf(address(this)) >= amount) { + if remoteChainID == block.chainid && IERC20(remoteAddress).balanceOf(address(this)) >= amount) { // Release escrowed native tokens require(IERC20(native).transfer(receiver, amount), "Native release failed"); token = native; From 5c2b17aa5b08dc3f933236aaa3b10f2c2800ab9a Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Mon, 20 Oct 2025 17:26:31 +0300 Subject: [PATCH 06/10] use factory --- universal_bridge.md | 113 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index 8113af4..24fc605 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -20,7 +20,7 @@ The Universal Shared Bridge for OP Chains enables seamless asset transfers betwe --- -## Introducing the ComposeableERC20 +## ComposeableERC20 Inspired by `OptimismSuperChainERC20`, this is an ERC7802 compliant token for cross-chain transfers. The code snippet below describe the main functionality. @@ -62,6 +62,12 @@ abstract contract ComposeableERC20 is ERC20, IERC7802 { } ``` +### Minting CET on the fly + +The universal + + + ___ # L2↔L2 Bridge --- Sessioned Mailbox Flow @@ -69,6 +75,9 @@ ___ ### Entities & Contracts +- **ComposableERC20** + An ERC20 wrapper native to the bridge. + - **Bridge (per L2)** Handles: locking native ERC-20, burning CET, mailbox write/read, and minting (via token's `onlyBridge` gate when called from the bridge @@ -103,6 +112,93 @@ ___ ------------------------------------------------------------------------ +## ComposeableERC20 + +Inspired by `OptimismSuperChainERC20`, this is an ERC7802 compliant token for cross-chain transfers. +The code snippet below describe the main functionality. + +```solidity +abstract contract ComposeableERC20 is ERC20, IERC7802 { + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external { + if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); + + _mint(_to, _amount); + + emit CrosschainMint(_to, _amount, msg.sender); + } + + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external { + if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); + + _burn(_from, _amount); + + emit CrosschainBurn(_from, _amount, msg.sender); + } + + /// @notice Storage struct for the BridgedComposeTokenERC20 metadata. + struct BridgedComposeTokenERC20Metadata { + /// @notice The ChainID where this token was originally minted. + uint256 remoteChainID + /// @notice Address of the corresponding version of this token on the remote chain. + address remoteAsset; + /// @notice Name of the token + string name; + /// @notice Symbol of the token + string symbol; + /// @notice Decimals of the token + uint8 decimals; + } +``` + +### Minting CET on the fly + +The bridge can mint CETs on demand via a factory + +```solidity +interface ICETFactory { + function predictAddress(address l1Asset) external view returns (address predicted); + function deployIfAbsent( + address l1Asset, + uint8 decimals, + string calldata name, + string calldata symbol, + address bridge + ) external returns (address deployed); +} + +ICETFactory public cetFactory; + +function computeCETAddress(address remoteAsset) internal view returns (address) { + return cetFactory.predictAddress(remoteAsset); +} + +function ensureCETAndMint( + address remoteAsset, + string calldata name, + string calldata symbol, + uint8 decimals, + address to, + uint256 amount +) internal returns (address cet) { + // 1) Predict deterministic address + address predicted = computeCETAddress(remoteAsset); + + // 2) Deploy if missing (CREATE3-based factory) + cet = cetFactory.deployIfAbsent(remoteAsset, decimals, name, symbol, address(this)); + require(cet == predicted, "CET address mismatch"); + + // 3) Mint via bridge-only path + IToken(cet).mint(to, amount); + return cet; +} +``` + +------------- + ### Mailbox Mailbox is a container of `Messages` divided into 2 boxes: @@ -323,6 +419,11 @@ function bridgeCETTo( ``` solidity function receiveTokens( MessageHeader msgHeader + // the following parameters are only needed if the proper CET token wasn't deployed + // TODO: should they be part of the message? + string calldata name, + string calldata symbol, + uint8 decimals ) external returns (address token, uint256 amount) { require(msg.sender == msgHeader.receiver, "Only receiver can claim"); require(msgHeader.chainDest == block.chainid, "Wrong destination chain"); @@ -332,15 +433,12 @@ function receiveTokens( bytes memory m = mailbox.read(MessageHeader(sessionIDchainSrc, receiver, sessionId, "SEND"); if (m.length == 0) revert("No SEND message"); - address readSender; - address readReceiver; + uint256 rmoteChainID; address remoteAsset; (remoteChainID, remoteAsset, amount) = abi.decode(m, (uint256, address, uint256)); - require(readSender == sender, "Sender mismatch"); - require(readReceiver == receiver, "Receiver mismatch"); // 2) RELEASE if native token is hosted & escrowed here, else MINT BCT if remoteChainID == block.chainid && IERC20(remoteAddress).balanceOf(address(this)) >= amount) { @@ -348,9 +446,8 @@ function receiveTokens( require(IERC20(native).transfer(receiver, amount), "Native release failed"); token = native; } else { - // Mint deterministic BCT on this chain - token = computeBCTAddress(remoteAsset); - ICET(token).crossChainMint(receiver, amount); + // Mint deterministic CET on this chain + token = ensureCETAndMint(remoteAddress, name, symbol, decimals, msgHeader.receiver, amount) } // 3) ACK back to source From 025f97c4d6e36de0b666417d5d3a0d1432900dfa Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Tue, 21 Oct 2025 15:31:40 +0300 Subject: [PATCH 07/10] draft --- universal_bridge.md | 49 --------------------------------------------- 1 file changed, 49 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index 24fc605..5f08dc7 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -18,55 +18,6 @@ The Universal Shared Bridge for OP Chains enables seamless asset transfers betwe 11. **Inter-L2 Fast Path:** For **L2↔L2** transfers, the destination **mints on receipt of a bridge message** (no proof verification at claim time). **Later settlement** is done simultaneously via aggregated proofs (out of scope here). 12. **TODO:** Allow token owner to have the bridge mint native token on specified conditions. ---- - -## ComposeableERC20 - -Inspired by `OptimismSuperChainERC20`, this is an ERC7802 compliant token for cross-chain transfers. -The code snippet below describe the main functionality. - -```solidity -abstract contract ComposeableERC20 is ERC20, IERC7802 { - /// @param _to Address to mint tokens to. - /// @param _amount Amount of tokens to mint. - function crosschainMint(address _to, uint256 _amount) external { - if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); - - _mint(_to, _amount); - - emit CrosschainMint(_to, _amount, msg.sender); - } - - /// @param _from Address to burn tokens from. - /// @param _amount Amount of tokens to burn. - function crosschainBurn(address _from, uint256 _amount) external { - if (msg.sender != COMPOSE_TOKEN_BRIDGE) revert Unauthorized(); - - _burn(_from, _amount); - - emit CrosschainBurn(_from, _amount, msg.sender); - } - - /// @notice Storage struct for the BridgedComposeTokenERC20 metadata. - struct BridgedComposeTokenERC20Metadata { - /// @notice The ChainID where this token was originally minted. - uint256 remoteChainID - /// @notice Address of the corresponding version of this token on the remote chain. - address remoteAsset; - /// @notice Name of the token - string name; - /// @notice Symbol of the token - string symbol; - /// @notice Decimals of the token - uint8 decimals; - } -``` - -### Minting CET on the fly - -The universal - - ___ From ae3c9458e3c8f274aaedc6ae7e064914116f0430 Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Tue, 21 Oct 2025 17:06:00 +0300 Subject: [PATCH 08/10] clarification on L1 bridge --- universal_bridge.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/universal_bridge.md b/universal_bridge.md index 5f08dc7..68e82a2 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -434,8 +434,11 @@ function receiveTokens( ## L1 <-> L2 Bridge For native rollups. +We need to utilize the current OP-contracts with minimal changes. Namely the `StandardBridge.sol`, `CrossDomainMessenger.sol`, and thei L1/L2 couterparts are neccessary. + Currently an OP rollup manage the L1<->L2 bridge via `OptimismPortal2` contract. This utilizes an `ETHLockbox` contract that locks all deposited ETH. Each native rollups using the universal bridge will deploy a `ComposePortal` (similar to `OptimismPortal) that will use a shared `ETHLockBox` and an `ERC20LockBox`. The sharing of a single `LockBox` will ensure that funds deposited on any chain can be withdrawn via another chain. +The `OptimismPortal2` generate `TransactionDeposited` events, that are captured on OP-GETH and are relayed to the standard OP-Bridge contracts. The `StandardBridge:finalizeBridgeERC20` call must be changed so it will mint `ComposableERC20s`. ## TODO: Can we do L1<->L2 bridge for external rollups @@ -448,6 +451,6 @@ Currently Op-Succinct Sequencers pick up `TransactionDeposited` Events to relay They check the address of the contract that originated the event. And perform a ZK proof that the event was included in the `recieptsRoot` In the case of an external interop sequencer, a malicious wrapped sequencer may send a non backed log. This won't be ZK proven but it will still become part of the external rollup canonical state. -The result will be that the connection with the Universal Shared Bridge will be severed. +The result will be that the connection with the Universal Shared Bridge will be severed. From 71a125257ec052b2de2938e09be1d5257dad191a Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Wed, 22 Oct 2025 11:53:06 +0300 Subject: [PATCH 09/10] empty ack --- universal_bridge.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/universal_bridge.md b/universal_bridge.md index 68e82a2..a2c84c7 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -223,7 +223,7 @@ All `SEND` payloads use a single canonical ABI encoding: #### ACK payload: - abi.encode("OK") + Just empty `{}` ------------------------------------------------------------------------ @@ -442,6 +442,8 @@ The `OptimismPortal2` generate `TransactionDeposited` events, that are captured ## TODO: Can we do L1<->L2 bridge for external rollups + + ### ETH Not w.o having liquidity available on the external rollup. From af3f584492574fbe0e3d94c269601a4d9689997a Mon Sep 17 00:00:00 2001 From: Gal Rogozinski Date: Wed, 22 Oct 2025 12:37:40 +0300 Subject: [PATCH 10/10] review --- universal_bridge.md | 46 +++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/universal_bridge.md b/universal_bridge.md index a2c84c7..4f97f3b 100644 --- a/universal_bridge.md +++ b/universal_bridge.md @@ -34,6 +34,8 @@ ___ minting (via token's `onlyBridge` gate when called from the bridge context). + It should be deployed with CREATE2 so it has the same address on all chains. + - **Mailbox (per L2)** Minimal append-only message bus with read-once semantics per `(chainSrc, recipient, sessionId, label)`. @@ -51,8 +53,7 @@ ___ - **Deterministic CET Addresses (Superchain-style)** Each CET contract is deployed **at the same address across all OP - L2s**, deterministically derived from the L1 canonical asset address - (using CREATE2/CREATE3). + L2s**, deterministically derived from the L1 canonical asset address using CREATE2. - This eliminates the need for a per-chain registry. - Mailbox payloads only need to carry the **`remoteToken` address**. @@ -119,6 +120,8 @@ interface ICETFactory { string calldata symbol, address bridge ) external returns (address deployed); + // Salt derivation for deterministic addresses + function computeSalt(address l1Asset) external pure returns (bytes32); } ICETFactory public cetFactory; @@ -195,6 +198,15 @@ However, every message in a cross-chain exchange mapping to a single atomic oper The first 16 bytes serve as version. Currently the only canonical version is 0. +Recommended way of generating `sessionID`: +``` +VERSION | keccak256(abi.encode( + sender, + nonce, + blockNumber, + salt)) << 240 +``` + #### Replay Protection The `Read` function will return an error if it will be invoked more than once with the same `MessageHeader` @@ -216,14 +228,14 @@ All `SEND` payloads use a single canonical ABI encoding: abi.encode( uint256 remoteChainID // The native chain of the transferred asset - address remoteAsset, // canonical asset address + address remoteAsset, // canonical asset address on the escrow chain uint256 amount // amount ) #### ACK payload: - Just empty `{}` + Just empty `{}`. The ACK message should just be present. If it is missing the bridge reverts. ------------------------------------------------------------------------ @@ -257,25 +269,16 @@ function bridgeCETTo( ``` solidity /// Process funds reception on the destination chain -/// @param chainSrc source chain identifier the funds are sent from -/// @param chainDest dest chain identifier the funds are sent to -/// @param sender address of the sender of the funds -/// @param receiver address of the receiver of the funds -/// @param sessionId identifier of the user session -/// @return token address of the token that was transferred +/// @param msgHeader the identifier you need to locate the message /// @return amount amount of tokens transferred function receiveTokens( - uint256 chainSrc, - uint256 chainDest, - address sender, - address receiver, - uint256 sessionId + MessageHeader msgHeader ) external returns (address token, uint256 amount); ``` > Note: receiveTokens` sits **on the Bridge contract** > (so that when it calls `CrossChainMint`, the token sees -> `msg.sender == Bridge` and respects the `onlyBridge` mint gate), while +> respects the `onlyBridge` mint gate), while > still requiring `msg.sender == receiver` to enforce "only receiver can > claim". @@ -402,7 +405,7 @@ function receiveTokens( } // 3) ACK back to source - mailbox.write(chainSrc, sender, sessionId, "ACK SEND", abi.encode("OK")); + mailbox.write(chainSrc, sender, sessionId, "ACK SEND", abi.encode({})); emit TokensReceived(token, amount); return (token, amount); @@ -421,7 +424,7 @@ function receiveTokens( ------------------------------------------------------------------------ -### End-to-End Lifecycle +### End-to-End L2<->L2 Lifecycle 1. Sender calls `bridgeERC20To` or `bridgeCETTo` with sessionId. 2. Source bridge locks/burns + writes `"SEND"` with L1 asset address. @@ -434,15 +437,14 @@ function receiveTokens( ## L1 <-> L2 Bridge For native rollups. -We need to utilize the current OP-contracts with minimal changes. Namely the `StandardBridge.sol`, `CrossDomainMessenger.sol`, and thei L1/L2 couterparts are neccessary. +We need to utilize the current OP-contracts with minimal changes. Namely the `StandardBridge.sol`, `CrossDomainMessenger.sol`. -Currently an OP rollup manage the L1<->L2 bridge via `OptimismPortal2` contract. This utilizes an `ETHLockbox` contract that locks all deposited ETH. Each native rollups using the universal bridge will deploy a `ComposePortal` (similar to `OptimismPortal) that will use a shared `ETHLockBox` and an `ERC20LockBox`. The sharing of a single `LockBox` will ensure that funds deposited on any chain can be withdrawn via another chain. +Currently an OP rollup manage the L1<->L2 bridge via `OptimismPortal2` contract. This utilizes an `ETHLockbox` contract that locks all deposited ETH. Each native rollups using the universal bridge will deploy its portal that will use a shared `ETHLockBox` and an `ERC20LockBox`. The sharing of a single `LockBox` will ensure that funds deposited on any chain can be withdrawn via another chain. The `OptimismPortal2` generate `TransactionDeposited` events, that are captured on OP-GETH and are relayed to the standard OP-Bridge contracts. The `StandardBridge:finalizeBridgeERC20` call must be changed so it will mint `ComposableERC20s`. -## TODO: Can we do L1<->L2 bridge for external rollups - +## TODO: Can we do L1<->L2 bridge for external rollups ### ETH Not w.o having liquidity available on the external rollup.