diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c0dd8638c8d9..ca34a5190d3a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -79,6 +79,10 @@ - [Fee Model](specs/zk_evm/fee_model.md) - [Precompiles](specs/zk_evm/precompiles.md) - [System Contracts](specs/zk_evm/system_contracts.md) +- [Interop](specs/interop/overview.md) + - [Interop Messages](specs/interop/interopmessages.md) + - [Bundles and Calls](specs/interop/bundlesandcalls.md) + - [Interop Transactions](specs/interop/interoptransactions.md) # Announcements diff --git a/docs/src/guides/build-docker.md b/docs/src/guides/build-docker.md index 5dd9cff022b9..6b0608275d8b 100644 --- a/docs/src/guides/build-docker.md +++ b/docs/src/guides/build-docker.md @@ -10,7 +10,8 @@ Install prerequisites: see ## Build docker files -You may build all images with [Makefile](../../docker/Makefile) located in [docker](../../docker) directory in this repository +You may build all images with [Makefile](../../docker/Makefile) located in [docker](../../docker) directory in this +repository > All commands should be run from the root directory of the repository diff --git a/docs/src/specs/img/autoexecution.png b/docs/src/specs/img/autoexecution.png new file mode 100644 index 000000000000..bef85e282012 Binary files /dev/null and b/docs/src/specs/img/autoexecution.png differ diff --git a/docs/src/specs/img/callride.png b/docs/src/specs/img/callride.png new file mode 100644 index 000000000000..568aab9c7242 Binary files /dev/null and b/docs/src/specs/img/callride.png differ diff --git a/docs/src/specs/img/chainop.png b/docs/src/specs/img/chainop.png new file mode 100644 index 000000000000..bd18e98b0e1a Binary files /dev/null and b/docs/src/specs/img/chainop.png differ diff --git a/docs/src/specs/img/finaltx.png b/docs/src/specs/img/finaltx.png new file mode 100644 index 000000000000..c3bd287c0465 Binary files /dev/null and b/docs/src/specs/img/finaltx.png differ diff --git a/docs/src/specs/img/gateway.png b/docs/src/specs/img/gateway.png new file mode 100644 index 000000000000..4d18d38ba338 Binary files /dev/null and b/docs/src/specs/img/gateway.png differ diff --git a/docs/src/specs/img/globalroot.png b/docs/src/specs/img/globalroot.png new file mode 100644 index 000000000000..3152cbec4e81 Binary files /dev/null and b/docs/src/specs/img/globalroot.png differ diff --git a/docs/src/specs/img/interopcall.png b/docs/src/specs/img/interopcall.png new file mode 100644 index 000000000000..b683122f1363 Binary files /dev/null and b/docs/src/specs/img/interopcall.png differ diff --git a/docs/src/specs/img/interopcallbundle.png b/docs/src/specs/img/interopcallbundle.png new file mode 100644 index 000000000000..fc82f02bc69c Binary files /dev/null and b/docs/src/specs/img/interopcallbundle.png differ diff --git a/docs/src/specs/img/interopmsg.png b/docs/src/specs/img/interopmsg.png new file mode 100644 index 000000000000..5469b1a4e3ff Binary files /dev/null and b/docs/src/specs/img/interopmsg.png differ diff --git a/docs/src/specs/img/interoptx.png b/docs/src/specs/img/interoptx.png new file mode 100644 index 000000000000..3d9fe295fe4e Binary files /dev/null and b/docs/src/specs/img/interoptx.png differ diff --git a/docs/src/specs/img/ipointers.png b/docs/src/specs/img/ipointers.png new file mode 100644 index 000000000000..60c29e601237 Binary files /dev/null and b/docs/src/specs/img/ipointers.png differ diff --git a/docs/src/specs/img/levelsofinterop.png b/docs/src/specs/img/levelsofinterop.png new file mode 100644 index 000000000000..5ee15946935d Binary files /dev/null and b/docs/src/specs/img/levelsofinterop.png differ diff --git a/docs/src/specs/img/msgdotsender.png b/docs/src/specs/img/msgdotsender.png new file mode 100644 index 000000000000..b411690785db Binary files /dev/null and b/docs/src/specs/img/msgdotsender.png differ diff --git a/docs/src/specs/img/paymastertx.png b/docs/src/specs/img/paymastertx.png new file mode 100644 index 000000000000..b3f25936b8dd Binary files /dev/null and b/docs/src/specs/img/paymastertx.png differ diff --git a/docs/src/specs/img/proofmerklepath.png b/docs/src/specs/img/proofmerklepath.png new file mode 100644 index 000000000000..beaaa175e1d0 Binary files /dev/null and b/docs/src/specs/img/proofmerklepath.png differ diff --git a/docs/src/specs/img/retryexample.png b/docs/src/specs/img/retryexample.png new file mode 100644 index 000000000000..6c79ef637340 Binary files /dev/null and b/docs/src/specs/img/retryexample.png differ diff --git a/docs/src/specs/img/sendtol1.png b/docs/src/specs/img/sendtol1.png new file mode 100644 index 000000000000..b98e51879f34 Binary files /dev/null and b/docs/src/specs/img/sendtol1.png differ diff --git a/docs/src/specs/img/verifyinteropmsg.png b/docs/src/specs/img/verifyinteropmsg.png new file mode 100644 index 000000000000..c59e5607f7da Binary files /dev/null and b/docs/src/specs/img/verifyinteropmsg.png differ diff --git a/docs/src/specs/interop/README.md b/docs/src/specs/interop/README.md new file mode 100644 index 000000000000..88e3d514b562 --- /dev/null +++ b/docs/src/specs/interop/README.md @@ -0,0 +1,6 @@ +# Interop + +- [Overview](./overview.md) +- [Interop Messages](./interopmessages.md) +- [Bundles and Calls](./bundlesandcalls.md) +- [Interop Transactions](./interoptransactions.md) diff --git a/docs/src/specs/interop/bundlesandcalls.md b/docs/src/specs/interop/bundlesandcalls.md new file mode 100644 index 000000000000..e49e10e8eed2 --- /dev/null +++ b/docs/src/specs/interop/bundlesandcalls.md @@ -0,0 +1,278 @@ +# Bundles and Calls + +## Basics Calls + +Interop Calls are the next level of interfaces, built on top of Interop Messages, enabling you to call contracts on +other chains. + +![interopcall.png](../img/interopcall.png) + +At this level, the system handles replay protection—once a call is successfully executed, it cannot be executed again +(eliminating the need for your own nullifiers or similar mechanisms). + +Additionally, these calls originate from aliased accounts, simplifying permission management (more details on this +below). + +Cancellations and retries are managed at the next level (Bundles), which are covered in the following section. + +### Interface + +On the sending side, the interface provides the option to send this "call" to the destination contract. + +```solidity +struct InteropCall { + address sourceSender, + address destinationAddress, + uint256 destinationChainId, + calldata data, + uint256 value +} +contract InteropCenter { + // On source chain. + // Sends a 'single' basic internal call to destination chain & address. + // Internally, it starts a bundle, adds this call and sends it over. + function sendCall(destinationChain, destinationAddress, calldata, msgValue) returns bytes32 bundleId; +} +``` + +In return, you receive a `bundleId` (we’ll explain bundles later, but for now, think of it as a unique identifier for +your call). + +On the destination chain, you can execute the call using the execute method: + +```solidity +contract InteropCenter { + // Executes a given bundle. + // interopMessage is the message that contains your bundle as payload. + // If it fails, it can be called again. + function executeInteropBundle(interopMessage, proof); + + // If the bundle didn't execute succesfully yet, it can be marked as cancelled. + // See details below. + function cancelInteropBundle(interopMessage, proof); +} + +``` + +You can retrieve the `interopMessage` (which contains your entire payload) from the Gateway, or you can construct it +yourself using L1 data. + +Under the hood, this process calls the `destinationAddress` with the specified calldata. + +This leads to an important question: **Who is the msg.sender for this call?** + +## `msg.sender` of the Destination Call + +The `msg.sender` on the destination chain will be the **AliasedAccount** — an address created as a hash of the original +sender and the original source chain. + +(Normally, we’d like to use `sourceAccount@sourceChain`, but since Ethereum limits the size of addresses to 20 bytes, we +compute the Keccak hash of the string above and use this as the address.) + +One way to think about it is this: You (as account `0x5bFF1...` on chain A) can send a call to a contract on a +destination chain, and for that contract, it will appear as if the call came locally from the address +`keccak(0x5bFF1 || A)`. This means you are effectively "controlling" such an account address on **every ZK Chain** by +sending interop messages from the `0x5bFF1...` account on chain A. + +![msgdotsender.png](../img/msgdotsender.png) + +## Simple Example + +Imagine you have contracts on chains B, C, and D, and you’d like them to send "reports" to the Headquarters (HQ) +contract on chain A every time a customer makes a purchase. + +```solidity +// Deployed on chains B, C, D. +contract Shop { + /// Called by the customers when they buy something. + function buy(uint256 itemPrice) { + // handle payment etc. + ... + // report to HQ + InteropCenter(INTEROP_ADDRESS).sendCall( + 324, // chain id of chain A, + 0xc425.., // HQ contract on chain A, + createCalldata("reportSales(uint256)", itemPrice), // calldata + 0, // no value + ); + } +} + +// Deployed on chain A +contract HQ { + // List of shops + mapping (address => bool) shops; + mapping (address => uint256) sales; + function addShop(address addressOnChain, uint256 chainId) onlyOwner { + // Adding aliased accounts. + shops[address(keccak(addressOnChain || chainId))] = true; + } + + function reportSales(uint256 itemPrice) { + // only allow calls from our shops (their aliased accounts). + require(shops[msg.sender]); + sales[msg.sender] += itemPrice; + } +} +``` + +#### Who is paying for gas? How does this Call get to the destination chain + +At this level, the **InteropCall** acts like a hitchhiker — it relies on someone (anyone) to pick it up, execute it, and +pay for the gas! + +![callride.png](../img/callride.png) + +While any transaction on the destination chain can simply call `InteropCenter.executeInteropBundle`, if you don’t want +to rely on hitchhiking, you can create one yourself. We’ll discuss this in the section about **Interop Transactions**. + +## Bundles + +Before we proceed to discuss **InteropTransactions**, there is one more layer in between: **InteropBundles**. + +![interopcallbundle.png](../img/interopcallbundle.png) + +**Bundles Offer:** + +- **Shared Fate**: All calls in the bundle either succeed or fail together. +- **Retries**: If a bundle fails, it can be retried (e.g., with more gas). +- **Cancellations**: If a bundle has not been successfully executed yet, it can be cancelled. + +If you look closely at the interface we used earlier, you’ll notice that we were already discussing the execution of +**Bundles** rather than single calls. So, let’s dive into what bundles are and the role they fulfill. + +The primary purpose of a bundle is to ensure that a given list of calls is executed in a specific order and has a shared +fate (i.e., either all succeed or all fail). + +In this sense, you can think of a bundle as a **"multicall"**, but with two key differences: + +1. You cannot "unbundle" items—an individual `InteropCall` cannot be run independently; it is tightly tied to the + bundle. + +2. Each `InteropCall` within a bundle can use a different aliased account, enabling separate permissions for each call. + +```solidity +contract InteropCenter { + struct InteropBundle { + // Calls have to be done in this order. + InteropCall calls[]; + uint256 destinationChain; + + // If not set - anyone can execute it. + address executionAddresses[]; + // Who can 'cancel' this bundle. + address cancellationAddress; + } + + // Starts a new bundle. + // All the calls that will be added to this bundle (potentially by different contracts) + // will have a 'shared fate'. + // The whole bundle must be going to a single destination chain. + function startBundle(destinationChain) returns bundleId; + // Adds a new call to the opened bundle. + // Returns the messageId of this single message in the bundle. + function addToBundle(bundleId, destinationAddress, calldata, msgValue) return msgHash; + // Finishes a given bundle, and sends it. + function finishAndSendBundle(bundleId) return msgHash; +} +``` + +### Cross Chain Swap Example + +Imagine you want to perform a swap on chain B, exchanging USDC for PEPE, but all your assets are currently on chain A. + +This process would typically involve four steps: + +1. Transfer USDC from chain A to chain B. +2. Set allowance for the swap. +3. Execute the swap. +4. Transfer PEPE back to chain A. + +Each of these steps is a separate "call," but you need them to execute in exactly this order and, ideally, atomically. +If the swap fails, you wouldn’t want the allowance to remain set on the destination chain. + +Below is an example of how this process could look (note that the code is pseudocode; we’ll explain the helper methods +required to make it work in a later section). + +```solidity +bundleId = InteropCenter(INTEROP_CENTER).startBundle(chainD); +// This will 'burn' the 1k USDC, create the special interopCall +// when this call is executed on chainD, it will mint 1k USDC there. +// BUT - this interopCall is tied to this bundle id. +USDCBridge.transferWithBundle( + bundleId, + chainD, + aliasedAccount(this(account), block.chain_id), + 1000); + + +// This will create interopCall to set allowance. +InteropCenter.addToBundle(bundleId, + USDCOnDestinationChain, + createCalldata("approve", 1000, poolOnDestinationChain), + 0); +// This will create interopCall to do the swap. +InteropCenter.addToBundle(bundleId, + poolOnDestinationChain, + createCalldata("swap", "USDC_PEPE", 1000, ...), + 0) +// And this will be the interopcall to transfer all the assets back. +InteropCenter.addToBundle(bundleId, + pepeBridgeOnDestinationChain, + createCalldata("transferAll", block.chain_id, this(account)), + 0) + + +bundleHash = interopCenter.finishAndSendBundle(bundleId); +``` + +In the code above, we created a bundle that anyone can execute on the destination chain. This bundle will handle the +entire process: minting, approving, swapping, and transferring back. + +### Bundle Restrictions + +When starting a bundle, if you specify the `executionAddress`, only that account will be able to execute the bundle on +the destination chain. If no `executionAddress` is specified, anyone can trigger the execution. + +## Retries and Cancellations + +If bundle execution fails — whether due to a contract error or running out of gas—none of its calls will be applied. The +bundle can be re-run on the **destination chain** without requiring any updates or notifications to the source chain. +More details about retries and gas will be covered in the next level, **Interop Transactions**. + +This process can be likened to a "hitchhiker" (or in the case of a bundle, a group of hitchhikers) — if the car they’re +traveling in doesn’t reach the destination, they simply find another ride rather than returning home. + +However, there are cases where the bundle should be cancelled. Cancellation can be performed by the +`cancellationAddress` specified in the bundle itself. + +#### For our cross chain swap example + +1. Call `cancelInteropBundle(interopMessage, proof)` on the destination chain. + - A helper method for this will be introduced in the later section. +2. When cancellation occurs, the destination chain will generate an `InteropMessage` containing cancellation + information. +3. Using the proof from this method, the user can call the USDC bridge to recover their assets: + +```solidity +USDCBridge.recoverFailedTransfer(bundleId, cancellationMessage, proof); +``` + +### Some details on our approach + +#### Destination Contract + +- On ElasticChain, the destination contract does not need to know it is being called via an interop call. Requests + arrive from `aliased accounts'. + +#### Batching + +- ElasticChain supports bundling of messages, ensuring shared fate and strict order. + +#### Execution Permissions + +- ElasticChain allows restricting who can execute the call or bundle on the destination chain. + +#### Cancellations + +- ElasticChain supports restricting who can cancel. Cancellation can happen at any time. diff --git a/docs/src/specs/interop/interopmessages.md b/docs/src/specs/interop/interopmessages.md new file mode 100644 index 000000000000..cabfd0e56750 --- /dev/null +++ b/docs/src/specs/interop/interopmessages.md @@ -0,0 +1,181 @@ +# Interop Messages + +In this section, we’re going to cover the lowest level of the interop stack: **Interop Messages** — the interface that +forms the foundation for everything else. + +We’ll explore the details of the interface, its use cases, and how it compares to similar interfaces from +Superchain/Optimism. + +This is an advanced document. While most users and app developers typically interact with higher levels of interop, it’s +still valuable to understand how the internals work. + +## Basics + +![interopmsg.png](../img/interopmsg.png) + +Interop Messages are the lowest level of our stack. + +An **InteropMessage** contains data and offers two methods: + +- Send a message +- Verify that a given message was sent on some chain + +Notice that the message itself doesn’t have any ‘destination chain’ or address—it is simply a payload that a user (or +contract) is creating. Think of it as a broadcast. + +The `InteropCenter` is a contract that is pre-deployed on all chains at a fixed address `0x00..1234`. + +```solidity +contract InteropCenter { + // Sends interop message. Can be called by anyone. + // Returns the unique interopHash. + function sendInteropMessage(bytes data) returns interopHash; + + // Interop message - uniquely identified by the hash of the payload. + struct InteropMessage { + bytes data; + address sender; // filled by InteropCenter + uint256 sourceChainId; // filled by InteropCenter + uint256 messageNum; // a 'nonce' to guarantee different hashes. + } + + // Verifies if such interop message was ever producted. + function verifyInteropMessage(bytes32 interopHash, Proof merkleProof) return bool; +} +``` + +When you call `sendInteropMessage`, the `InteropCenter` adds additional fields, such as your sender address, source +chain ID, and messageNum (a nonce ensuring the hash of this structure is globally unique). It then returns the +`interopHash`. + +This `interopHash` serves as a globally unique identifier that can be used on any chain in the network to call +`verifyInteropMessage`. + +![A message created on one chain can be verified on any other chain.](../img/verifyinteropmsg.png) + +#### How do I get the proof + +You’ll notice that **verifyInteropMessage** has a second argument — a proof that you need to provide. This proof is a +Merkle tree proof (more details below). You can obtain it by querying the Settlement Layer (Gateway) or generating it +off-chain by examining the Gateway state on L1. + +#### How does the interop message differ from other layers (InteropTransactions, InteropCalls) + +As the most basic layer, an interop message doesn’t include any advanced features — it lacks support for selecting +destination chains, nullifiers/replay, cancellation, and more. + +If you need these capabilities, consider integrating with a higher layer of interop, such as Call or Bundle, which +provide these additional functionalities. + +## Simple Use Case + +Before we dive into the details of how the system works, let’s look at a simple use case for a DApp that decides to use +InteropMessage. + +For this example, imagine a basic cross-chain contract where the `signup()` method can be called on chains B, C, and D +only if someone has first called `signup_open()` on chain A. + +```solidity +// Contract deployed on chain A. +contract SignupManager { + public bytes32 sigup_open_msg_hash; + function signup_open() onlyOwner { + // We are open for business + signup_open_msg_hash = InteropCenter(INTEROP_CENTER_ADDRESS).sendInteropMessage("We are open"); + } +} + +// Contract deployed on all other chains. +contract SignupContract { + public bool signupIsOpen; + // Anyone can call it. + function openSignup(InteropMessage message, InteropProof proof) { + InteropCenter(INTEROP_CENTER_ADDRESS).verifyInteropMessage(keccak(message), proof); + require(message.sourceChainId == CHAIN_A_ID); + require(message.sender == SIGNUP_MANAGER_ON_CHAIN_A); + require(message.data == "We are open"); + signupIsOpen = true; + } + + function signup() { + require(signupIsOpen); + signedUpUser[msg.sender] = true; + } +} +``` + +In the example above, the `signupManager` on chain A calls the `signup_open` method. After that, any user on other +chains can retrieve the `signup_open_msg_hash`, obtain the necessary proof from the Gateway (or another source), and +call the `openSignup` function on any destination chain. + +## Deeper Technical Dive + +Let’s break down what happens inside the InteropCenter when a new interop message is created: + +```solidity +function sendInteropMessage(bytes data) { + messageNum += 1; + msg = InteropMessage({data, msg.sender, block.chain_id, messageNum}); + // Does L2->L1 Messaging. + sendToL1(abi.encode(msg)); + return keccak(msg); +} +``` + +As you can see, it populates the necessary data and then calls the `sendToL1` method. + +The `sendToL1` method is part of a system contract that gathers all messages during a batch, constructs a Merkle tree +from them at the end of the batch, and sends this tree to the SettlementLayer (Gateway) when the batch is committed. + +![sendtol1.png](../img/sendtol1.png) + +The Gateway will verify the hashes of the messages to ensure it has received the correct preimages. Once the proof for +the batch is submitted (or more accurately, during the "execute" step), it will add the root of the Merkle tree to its +`globalRoot`. + +![globalroot.png](../img/globalroot.png) + +The `globalRoot` is the root of the Merkle tree that includes all messages from all chains. Each chain regularly reads +the globalRoot value from the Gateway to stay synchronized. + +![gateway.png](../img/gateway.png) + +If a user wants to call `verifyInteropMessage` on a chain, they first need to query the Gateway for the Merkle path from +the batch they are interested in up to the `globalRoot`. Once they have this path, they can provide it as an argument +when calling a method on the destination chain (such as the `openSignup` method in our example). + +![proofmerklepath.png](../img/proofmerklepath.png) + +#### What if the Gateway doesn’t respond + +If the Gateway doesn’t respond, users can manually re-create the Merkle proof using data available on L1. Every +interopMessage is also sent to L1. + +#### Global roots change frequently + +Yes, global roots update continuously as new chains prove their blocks. However, chains retain historical global roots +for a reasonable period (around 24 hours) to ensure that recently generated Merkle paths remain valid. + +#### Is this secure? Could a chain operator, like Chain D, use a different global root + +Yes, it’s secure. If a malicious operator on Chain D attempted to use a different global root, they wouldn’t be able to +submit the proof for their new batch to the Gateway. This is because the proof’s public inputs must include the valid +global root. + +#### What if the Gateway is malicious + +If the Gateway behaves maliciously, it wouldn’t be able to submit its batches to L1, as the proof would fail +verification. A separate section will cover interop transaction security in more detail. + +### Other Features + +#### Dependency Set + +- In ElasticChain, this is implicitly handled by the Gateway. Any chain that is part of the global root can exchange + messages with any other chain, effectively forming an undirected graph. + +#### Timestamps and Expiration + +- In ElasticChain, older messages become increasingly difficult to validate as it becomes harder to gather the data + required to construct a Merkle proof. Expiration is also being considered for this reason, but the specifics are yet + to be determined. diff --git a/docs/src/specs/interop/interoptransactions.md b/docs/src/specs/interop/interoptransactions.md new file mode 100644 index 000000000000..feccc49ea91a --- /dev/null +++ b/docs/src/specs/interop/interoptransactions.md @@ -0,0 +1,196 @@ +# Interop Transactions + +## Basics + +The **InteropTransaction** sits at the top of our interop stack, acting as the “delivery” mechanism for **Interop +Bundles**. + +Think of it like a car that picks up our "hitchhiker" bundles and carries them to their destination. + +![interoptx.png](../img/interoptx.png) + +**Note:** Interop Transactions aren’t the only way to execute a bundle. Once an interop bundle is created on the source +chain, users can simply send a regular transaction on the destination chain to execute it. + +However, this approach can be inconvenient as it requires users to have funds on the destination chain to cover gas fees +and to configure the necessary network settings (like the RPC address). + +**InteropTransactions** simplify this process by handling everything from the source chain. They allow you to select +which **interopBundle** to execute, specify gas details (such as gas amount and gas price), and determine who will cover +the gas costs. This can be achieved using tokens on the source chain or through a paymaster. + +Once configured, the transaction will automatically execute, either by the chain operator, the gateway, or off-chain +tools. + +An **InteropTransaction** contains two pointers to bundles: + +- **feesBundle**: Holds interop calls to cover fees. +- **bundleHash**: Contains the main execution. + +![ipointers.png](../img/ipointers.png) + +## Interface + +The function `sendInteropTransaction` provides all the options. For simpler use cases, refer to the helper methods +defined later in the article. + +```solidity +contract InteropCenter { + /// Creates a transaction that will attempt to execute a given Bundle on the destination chain. + /// Such transaction can be 'picked up' by the destination chain automatically. + /// This function covers all the cases - we expect most users to use the helper + /// functions defined later. + function sendInteropTransaction( + destinationChain, + bundleHash, // the main bundle that you want to execute on destination chain + gasLimit, // gasLimit & price for execution + gasPrice, + feesBundleHash, // this is the bundle that contains the calls to pay for gas + destinationPaymaster, // optionally - you can use a paymaster on destination chain + destinationPaymasterInput); // with specific params + + + struct InteropTransaction { + address sourceChainSender + uint256 destinationChain + uint256 gasLimit; + uint256 gasPrice; + uint256 value; + bytes32 bundleHash; + bytes32 feesBundleHash; + address destinationPaymaster; + bytes destinationPaymasterInput; + } +} +``` + +After creating the **InteropBundle**, you can simply call `sendInteropTransaction` to create the complete transaction +that will execute the bundle. + +## Retries + +If your transaction fails to execute the bundle (e.g., due to a low gas limit) or isn’t included at all (e.g., due to +too low gasPrice), you can send another transaction to **attempt to execute the same bundle again**. + +Simply call `sendInteropTransaction` again with updated gas settings. + +### Example of Retrying + +Here’s a concrete example: Suppose you created a bundle to perform a swap that includes transferring 100 ETH, executing +the swap, and transferring some tokens back. + +You attempted to send the interop transaction with a low gas limit (e.g., 100). Since you didn’t have any base tokens on +the destination chain, you created a separate bundle to transfer a small fee (e.g., 0.0001) to cover the gas. + +You sent your first interop transaction to the destination chain, but it failed due to insufficient gas. However, your +“fee bundle” was successfully executed, as it covered the gas cost for the failed attempt. + +Now, you have two options: either cancel the execution bundle (the one with 100 ETH) or retry. + +To retry, you decide to set a higher gas limit (e.g., 10,000) and create another fee transfer (e.g., 0.01) but use **the +same execution bundle** as before. + +This time, the transaction succeeds — the swap completes on the destination chain, and the resulting tokens are +successfully transferred back to the source chain. + +![retryexample.png](../img/retryexample.png) + +## Fees & Restrictions + +Using an **InteropBundle** for fee payments offers flexibility, allowing users to transfer a small amount to cover the +fees while keeping the main assets in the execution bundle itself. + +### Restrictions + +This flexibility comes with trade-offs, similar to the validation phases in **Account Abstraction** or **ERC4337**, +primarily designed to prevent DoS attacks. Key restrictions include: + +- **Lower gas limits** +- **Limited access to specific slots** + +Additionally, when the `INTEROP_CENTER` constructs an **InteropTransaction**, it enforces extra restrictions on +**feePaymentBundles**: + +- **Restricted Executors**: + Only your **AliasedAccount** on the receiving side can execute the `feePaymentBundle`. + +This restriction is crucial for security, preventing others from executing your **fee bundle**, which could cause your +transaction to fail and prevent the **execution bundle** from processing. + +### **Types of Fees** + +#### Using the Destination Chain’s Base Token + +The simplest scenario is when you (as the sender) already have the destination chain’s base token available on the +source chain. + +For example: + +- If you are sending a transaction from **Era** (base token: ETH) to **Sophon** (base token: SOPH) and already have SOPH + on ERA, you can use it for the fee. + +To make this easier, we’ll provide a helper function: + +```solidity +contract InteropCenter { + // Creates InteropTransaction to the destination chain with payment with base token. + // Before calling, you have to 'approve' InteropCenter to the ERC20/Bridge that holds the destination chain's base tokens. + // or if the destination chain's tokens are the same as yours, just attach value to this call. + function sendInteropTxMinimal( + destinationChain, + bundleHash, // the main bundle that you want to execute on destination chain + gasLimit, // gasLimit & price for execution + gasPrice, + ); + } +``` + +#### Using paymaster on the destination chain + +If you don’t have the base token from the destination chain (e.g., SOPH in our example) on your source chain, you’ll +need to use a paymaster on the destination chain instead. + +In this case, you’ll send the token you do have (e.g., USDC) to the destination chain as part of the **feeBundleHash**. +Once there, you’ll use it to pay the paymaster on the destination chain to cover your gas fees. + +Your **InteropTransaction** would look like this: + +![paymastertx.png](../img/paymastertx.png) + +## **Automatic Execution** + +One of the main advantages of **InteropTransactions** is that they execute automatically. As the sender on the source +chain, you don’t need to worry about technical details like RPC addresses or obtaining proofs — it’s all handled for +you. + +After creating an **InteropTransaction**, it can be relayed to the destination chain by anyone. The transaction already +includes a signature (also known as an interop message proof), making it fully self-contained and ready to send without +requiring additional permissions. + +Typically, the destination chain’s operator will handle and include incoming **InteropTransactions**. However, if they +don’t, the **Gateway** or other participants can step in to prepare and send them. + +You can also use the available tools to create and send the destination transaction yourself. Since the transaction is +self-contained, it doesn’t require additional funds or signatures to execute. + +![Usually destination chain operator will keep querying gateway to see if there are any messages for their chain.](../img/autoexecution.png) + +Once they see the message, they can request the proof from the **Gateway** and also fetch the **InteropBundles** +contained within the message (along with their respective proofs). + +![Operator getting necessary data from Gateway.](../img/chainop.png) + +As the final step, the operator can use the received data to create a regular transaction, which can then be sent to +their chain. + +![Creating the final transaction to send to the destination chain](../img/finaltx.png) + +The steps above don’t require any special permissions and can be executed by anyone. + +While the **Gateway** was used above for tasks like providing proofs, if the Gateway becomes malicious, all this +information can still be constructed off-chain using data available on L1. + +### How it Works Under the hood + +We’ll modify the default account to accept interop proofs as signatures, seamlessly integrating with the existing ZKSync +native **Account Abstraction** model. diff --git a/docs/src/specs/interop/overview.md b/docs/src/specs/interop/overview.md new file mode 100644 index 000000000000..8ca28723e03a --- /dev/null +++ b/docs/src/specs/interop/overview.md @@ -0,0 +1,166 @@ +# Intro Guide to Interop + +## What is Interop + +Interop is a way to communicate and transact between two ZK Stack chains. It allows you to: + +**1. Observe messages:** Track when an interop message (think of it as a special event) is created on the source chain. + +**2. Send assets:** Transfer ERC20 tokens and other assets between chains. + +**3. Execute calls:** Call a contract on a remote chain with specific calldata and value. + +With interop, you automatically get an account (a.k.a. aliasedAccount) on each chain, which you can control from the +source chain. + +**4. Execute bundles of calls:** Group multiple remote calls into a single bundle, ensuring all of them execute at once. + +**5. Execute transactions:** Create transactions on the source chain, which will automatically get executed on the +destination chain, with options to choose from various cross-chain Paymaster solutions to handle gas fees. + +## How to Use Interop + +Here’s a simple example of calling a contract on a destination chain: + +```solidity +cast send source-chain-rpc.com INTEROP_CENTER_ADDRESS sendInteropWithSingleCall( + 0x1fa72e78 // destination_chain_id, + 0xb4AB2FF34fa... // destination_contract, + 0x29723511000000... // destination_calldata, + 0, // value + 100_000, // gasLimit + 250_000_000, // gasPrice + ) +``` + +While this looks very similar to a 'regular' call, there are some nuances, especially around handling failures and +errors. + +Let’s explore these key details together. + +## Common Questions and Considerations + +#### 1. Who pays for gas + +When using this method, your account must hold `gasLimit * gasPrice` worth of destination chain tokens on the source +chain. + +For example, if you’re sending the request from Era and the destination chain is Sophon (with SOPH tokens), you’ll need +SOPH tokens available on Era. + +Additional payment options are available, which will be covered in later sections. + +#### 2. How does the destination contract know it’s from me + +The destination contract will see `msg.sender` as `keccak(source_account, source_chain)[:20]`. + +Ideally, we would use something like `source_account@source_chain` (similar to an email format), but since Ethereum +addresses are limited to 20 bytes, we use a Keccak hash to fit this constraint. + +#### 3. Who executes it on the destination chain + +The call is auto-executed on the destination chain. As a user, you don’t need to take any additional actions. + +#### 4. What if it runs out of gas or the gasPrice is set too low + +In either scenario, you can retry the transaction using the `retryInteropTransaction` method: + +```solidity + cast send source-chain.com INTEROP_CENTER_ADDRESS retryInteropTransaction( + 0x2654.. // previous interop transaction hash from above + 200_000, // new gasLimit + 300_000_000 // new gasPrice + ) +``` + +**Important** : Depending on your use case, it’s crucial to retry the transaction rather than creating a new one with +`sendInteropWithSingleCall`. + +For example, if your call involves transferring a large amount of assets, initiating a new `sendInteropWithSingleCall` +could result in freezing or burning those assets again. + +#### 5. What if my assets were burned during the transaction, but it failed on the destination chain? How do I get them back + +If your transaction fails on the destination chain, you can either: + +1. Retry the transaction with more gas or a higher gas limit (refer to the retry method above). + +2. Cancel the transaction using the following method: + +```solidity +cast send source-chain INTEROP_CENTER_ADDRESS cancelInteropTransaction( + 0x2654.., // previous interop transaction + 100_000, // gasLimit (cancellation also requires gas, but only to mark it as cancelled) + 300_000_000 // gasPrice +) +``` + +After cancellation, call the claimFailedDeposit method on the source chain contracts to recover the burned assets. Note +that the details for this step may vary depending on the contract specifics. + +## Complex Scenario + +#### 6. What if I want to transfer USDC to the Sophon chain, swap it for PEPE coin, and transfer the results back + +To accomplish this, you’ll need to: + +- Create multiple **InteropCalls** (e.g., transferring USDC, executing the swap). +- Combine these calls into a single **InteropBundle**. +- Execute the **InteropTransaction** on the destination chain. + +The step-by-step process and exact details will be covered in the next section. + +## Technical Details + +### How is Interop Different from a Bridge + +Bridges generally fall into two categories: Native and Third-Party. + +#### 1. Native Bridges + +Native bridges enable asset transfers “up and down” (from L2 to L1 and vice versa). In contrast, interop allows direct +transfers between different L2s. + +Instead of doing a "round trip" (L2 → L1 → another L2), interop lets you move assets directly between two L2s, saving +both time and cost. + +#### 2. Third-Party Bridging + +Third-party bridges enable transfers between two L2s, but they rely on their own liquidity. While you, as the user, +receive assets on the destination chain instantly, these assets come from the bridge’s liquidity pool. + +Bridge operators then rebalance using native bridging, which requires maintaining token reserves on both sides. This +adds costs for the bridge operators, often resulting in higher fees for users. + +The good news is that third-party bridges can use interop to improve their token transfers by utilizing the +**InteropMessage** layer. + +More details on this will follow below. + +### How Fast is It + +Interop speed is determined by its lowest level: **InteropMessage** propagation speed. This essentially depends on how +quickly the destination chain can confirm that the message created by the source chain is valid. + +- **Default Mode:** To prioritize security, the default interop mode waits for a ZK proof to validate the message, which + typically takes around 10 minutes. + +- **Fast Mode (Planned):** We are developing an alternative **INTEROP_CENTER** contract (using a different address but + the same interface) that will operate within 1 second. However, this faster mode comes with additional risks, similar + to the approach used by optimistic chains. + +### 4 Levels of Interop + +When analyzing interop, it can be broken into four levels, allowing you to choose the appropriate level for integration: + +- **InteropMessages:** The lowest level, directly used by third-party bridges and other protocols. + +- **InteropCall:** A medium level, designed for use by "library" contracts. + +- **InteropCallBundle:** A higher level, intended for use by "user-visible" contracts. + +- **InteropTransaction:** The highest level, designed for use in UX and frontends. + +![levelsofinterop.png](../img/levelsofinterop.png) + +We will be covering the details of each layer in the next section. diff --git a/install b/install new file mode 100755 index 000000000000..8bf36687e20d --- /dev/null +++ b/install @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -eo pipefail + +echo "Installing foundryup-zksync..." + +BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" +FOUNDRY_DIR="${FOUNDRY_DIR-"$BASE_DIR/.foundry"}" +FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" +FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1" + +BIN_URL="https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/foundryup-zksync/foundryup-zksync" +BIN_PATH="$FOUNDRY_BIN_DIR/foundryup-zksync" + +# Create the .foundry bin directory and foundryup binary if it doesn't exist. +mkdir -p "$FOUNDRY_BIN_DIR" +curl -sSf -L "$BIN_URL" -o "$BIN_PATH" +chmod +x "$BIN_PATH" + +# Create the man directory for future man files if it doesn't exist. +mkdir -p "$FOUNDRY_MAN_DIR" + +# Store the correct profile file (i.e. .profile for bash or .zshenv for ZSH). +case $SHELL in +*/zsh) + PROFILE="${ZDOTDIR-"$HOME"}/.zshenv" + PREF_SHELL=zsh + ;; +*/bash) + PROFILE=$HOME/.bashrc + PREF_SHELL=bash + ;; +*/fish) + PROFILE=$HOME/.config/fish/config.fish + PREF_SHELL=fish + ;; +*/ash) + PROFILE=$HOME/.profile + PREF_SHELL=ash + ;; +*) + echo "foundryup-zksync: could not detect shell, manually add ${FOUNDRY_BIN_DIR} to your PATH." + exit 1 +esac + +# Only add foundryup-zksync if it isn't already in PATH. +if [[ ":$PATH:" != *":${FOUNDRY_BIN_DIR}:"* ]]; then + # Add the foundryup directory to the path and ensure the old PATH variables remain. + # If the shell is fish, echo fish_add_path instead of export. + if [[ "$PREF_SHELL" == "fish" ]]; then + echo >> "$PROFILE" && echo "fish_add_path -a $FOUNDRY_BIN_DIR" >> "$PROFILE" + else + echo >> "$PROFILE" && echo "export PATH=\"\$PATH:$FOUNDRY_BIN_DIR\"" >> "$PROFILE" + fi +fi + +# Warn MacOS users that they may need to manually install libusb via Homebrew: +if [[ "$OSTYPE" =~ ^darwin ]] && [[ ! -f /usr/local/opt/libusb/lib/libusb-1.0.0.dylib && ! -f /opt/homebrew/opt/libusb/lib/libusb-1.0.0.dylib ]]; then + echo && echo "warning: libusb not found. You may need to install it manually on MacOS via Homebrew (brew install libusb)." +fi + +echo +echo "Detected your preferred shell is $PREF_SHELL and added foundryup-zksync to PATH." +echo "Run 'source $PROFILE' or start a new terminal session to use foundryup-zksync." +echo "Then, simply run 'foundryup-zksync' to install foundry-zksync."