Skip to content

Commit

Permalink
separate out two more examples
Browse files Browse the repository at this point in the history
  • Loading branch information
kelemeno committed Feb 12, 2025
1 parent d071126 commit e84c0d8
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 126 deletions.
2 changes: 1 addition & 1 deletion docs/src/specs/contracts/bridging/interop/bundles_calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ your call).
On the destination chain, you can execute the call using the execute method:

```solidity
contract InteropCenter {
contract InteropHandler {
// Executes a given bundle.
// interopMessage is the message that contains your bundle as payload.
// If it fails, it can be called again.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Interop Message 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.
Original file line number Diff line number Diff line change
@@ -1 +1,81 @@
# InteropCenter requestL2TransactionTwoBridges
# InteropCenter requestL2TransactionTwoBridges

## Generic usage of `BridgeHub.requestL2TransactionTwoBridges`

`L1AssetRouter` is the only bridge that can handle base tokens. However, the `BridgeHub.requestL2TransactionTwoBridges` could be used by `secondBridgeAddress` on L1. A notable example of how it is done is how our [CTMDeploymentTracker](../../../l1-contracts/contracts/bridgehub/CTMDeploymentTracker.sol) uses it to register the correct CTM address on Gateway. You can read more about how Gateway works in [its documentation](../../gateway/overview.md).

Let’s do a quick recap on how it works:

When calling `BridgeHub.requestL2TransactionTwoBridges` the following struct needs to be provided:

```solidity
struct L2TransactionRequestTwoBridgesOuter {
uint256 chainId;
uint256 mintValue;
uint256 l2Value;
uint256 l2GasLimit;
uint256 l2GasPerPubdataByteLimit;
address refundRecipient;
address secondBridgeAddress;
uint256 secondBridgeValue;
bytes secondBridgeCalldata;
}
```

- `secondBridgeAddress` is the address of the L1 contract that needs to perform the L1->L2 transaction.
- `secondBridgeValue` is the `msg.value` to be sent to the `secondBridgeAddress`.
- `secondBridgeCalldata` is the data to pass to the `secondBridgeAddress`. This can be interpreted any way it wants.

1. Firstly, the Bridgehub will deposit the `request.mintValue` the same way as during a simple L1→L2 transaction. These funds will be used for funding the `l2Value` and the fee to the operator.
2. After that, the `secondBridgeAddress.bridgehubDeposit` with the following signature is called

```solidity
struct L2TransactionRequestTwoBridgesInner {
// Should be equal to a constant `keccak256("TWO_BRIDGES_MAGIC_VALUE")) - 1`
bytes32 magicValue;
// The L2 contract to call
address l2Contract;
// The calldata to call it with
bytes l2Calldata;
// The factory deps to call it with
bytes[] factoryDeps;
// Just some 32-byte value that can be used for later processing
// It is called `txDataHash` as it *should* be used as a way to facilitate
// reclaiming failed deposits.
bytes32 txDataHash;
}
function bridgehubDeposit(
uint256 _chainId,
// The actual user that does the deposit
address _prevMsgSender,
// The msg.value of the L1->L2 transaction to be created
uint256 _l2Value,
// Custom bridge-specific data
bytes calldata _data
) external payable returns (L2TransactionRequestTwoBridgesInner memory request);
```

Now the job of the contract will be to “validate” whether they are okay with the transaction to come. For instance, the `CTMDeploymentTracker` checks that the `_prevMsgSender` is the owner of `CTMDeploymentTracker` and has the necessary rights to perform the transaction out of the name of it.

Ultimately, the correctly processed `bridgehubDeposit` function basically grants `BridgeHub` the right to create an L1→L2 transaction out of the name of the `secondBridgeAddress`. Since it is so powerful, the first returned value must be a magical constant that is equal to `keccak256("TWO_BRIDGES_MAGIC_VALUE")) - 1`. The fact that it was a somewhat non standard signature and a struct with the magical value is the major defense against “accidental” approvals to start a transaction out of the name of an account.

Aside from the magical constant, the method should also return the information an L1→L2 transaction will start its call with: the `l2Contract` , `l2Calldata`, `factoryDeps`. It also should return the `txDataHash` field. The meaning `txDataHash` will be needed in the next paragraphs. But generally it can be any 32-byte value the bridge wants.

1. After that, an L1→L2 transaction is invoked. Note, that the “trusted” `L1AssetRouter` has enforced that the baseToken was deposited correctly (again, the step (1) can _only_ be handled by the `L1AssetRouter`), while the second bridge can provide any data to call its L2 counterpart with.
2. As a final step, following function is called:

```solidity
function bridgehubConfirmL2Transaction(
// `chainId` of the ZKChain
uint256 _chainId,
// the same value that was returned by `bridgehubDeposit`
bytes32 _txDataHash,
// the hash of the L1->L2 transaction
bytes32 _txHash
) external;
```

This function is needed for whatever actions are needed to be done after the L1→L2 transaction has been invoked.

On `L1AssetRouter` it is used to remember the hash of each deposit transaction, so that later on, the funds could be returned to user if the `L1->L2` transaction fails. The `_txDataHash` is stored so that the whenever the users will want to reclaim funds from a failed deposit, they would provide the token and the amount as well as the sender to send the money to.
79 changes: 0 additions & 79 deletions docs/src/specs/contracts/bridging/interop/interop_center.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,85 +58,6 @@ This call will return the parameters to call the l2 contract with (the address o

![requestL2TransactionTwoBridges (SharedBridge) (1).png](../img/requestL2TransactionTwoBridges_depositEthToUSDC.png)

## Generic usage of `BridgeHub.requestL2TransactionTwoBridges`

`L1AssetRouter` is the only bridge that can handle base tokens. However, the `BridgeHub.requestL2TransactionTwoBridges` could be used by `secondBridgeAddress` on L1. A notable example of how it is done is how our [CTMDeploymentTracker](../../../l1-contracts/contracts/bridgehub/CTMDeploymentTracker.sol) uses it to register the correct CTM address on Gateway. You can read more about how Gateway works in [its documentation](../../gateway/overview.md).

Let’s do a quick recap on how it works:

When calling `BridgeHub.requestL2TransactionTwoBridges` the following struct needs to be provided:

```solidity
struct L2TransactionRequestTwoBridgesOuter {
uint256 chainId;
uint256 mintValue;
uint256 l2Value;
uint256 l2GasLimit;
uint256 l2GasPerPubdataByteLimit;
address refundRecipient;
address secondBridgeAddress;
uint256 secondBridgeValue;
bytes secondBridgeCalldata;
}
```

- `secondBridgeAddress` is the address of the L1 contract that needs to perform the L1->L2 transaction.
- `secondBridgeValue` is the `msg.value` to be sent to the `secondBridgeAddress`.
- `secondBridgeCalldata` is the data to pass to the `secondBridgeAddress`. This can be interpreted any way it wants.

1. Firstly, the Bridgehub will deposit the `request.mintValue` the same way as during a simple L1→L2 transaction. These funds will be used for funding the `l2Value` and the fee to the operator.
2. After that, the `secondBridgeAddress.bridgehubDeposit` with the following signature is called

```solidity
struct L2TransactionRequestTwoBridgesInner {
// Should be equal to a constant `keccak256("TWO_BRIDGES_MAGIC_VALUE")) - 1`
bytes32 magicValue;
// The L2 contract to call
address l2Contract;
// The calldata to call it with
bytes l2Calldata;
// The factory deps to call it with
bytes[] factoryDeps;
// Just some 32-byte value that can be used for later processing
// It is called `txDataHash` as it *should* be used as a way to facilitate
// reclaiming failed deposits.
bytes32 txDataHash;
}
function bridgehubDeposit(
uint256 _chainId,
// The actual user that does the deposit
address _prevMsgSender,
// The msg.value of the L1->L2 transaction to be created
uint256 _l2Value,
// Custom bridge-specific data
bytes calldata _data
) external payable returns (L2TransactionRequestTwoBridgesInner memory request);
```

Now the job of the contract will be to “validate” whether they are okay with the transaction to come. For instance, the `CTMDeploymentTracker` checks that the `_prevMsgSender` is the owner of `CTMDeploymentTracker` and has the necessary rights to perform the transaction out of the name of it.

Ultimately, the correctly processed `bridgehubDeposit` function basically grants `BridgeHub` the right to create an L1→L2 transaction out of the name of the `secondBridgeAddress`. Since it is so powerful, the first returned value must be a magical constant that is equal to `keccak256("TWO_BRIDGES_MAGIC_VALUE")) - 1`. The fact that it was a somewhat non standard signature and a struct with the magical value is the major defense against “accidental” approvals to start a transaction out of the name of an account.

Aside from the magical constant, the method should also return the information an L1→L2 transaction will start its call with: the `l2Contract` , `l2Calldata`, `factoryDeps`. It also should return the `txDataHash` field. The meaning `txDataHash` will be needed in the next paragraphs. But generally it can be any 32-byte value the bridge wants.

1. After that, an L1→L2 transaction is invoked. Note, that the “trusted” `L1AssetRouter` has enforced that the baseToken was deposited correctly (again, the step (1) can _only_ be handled by the `L1AssetRouter`), while the second bridge can provide any data to call its L2 counterpart with.
2. As a final step, following function is called:

```solidity
function bridgehubConfirmL2Transaction(
// `chainId` of the ZKChain
uint256 _chainId,
// the same value that was returned by `bridgehubDeposit`
bytes32 _txDataHash,
// the hash of the L1->L2 transaction
bytes32 _txHash
) external;
```

This function is needed for whatever actions are needed to be done after the L1→L2 transaction has been invoked.

On `L1AssetRouter` it is used to remember the hash of each deposit transaction, so that later on, the funds could be returned to user if the `L1->L2` transaction fails. The `_txDataHash` is stored so that the whenever the users will want to reclaim funds from a failed deposit, they would provide the token and the amount as well as the sender to send the money to.

## Claiming failed deposits

Expand Down
48 changes: 4 additions & 44 deletions docs/src/specs/contracts/bridging/interop/interop_messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ This `interopHash` serves as a globally unique identifier that can be used on an
#### 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
[chain](https://docs.zksync.io/build/api-reference/zks-rpc#zks_getl2tol1msgproof) , or generate it off-chain - by
Merkle tree proof (more details [here](./message_root.md)). You can obtain it by querying the chain using the
[api](https://docs.zksync.io/build/api-reference/zks-rpc#zks_getl2tol1msgproof), or generate it off-chain - by
looking at the chain's state on L1.

#### How does the interop message differ from other layers (InteropTransactions, InteropCalls)
Expand All @@ -65,48 +65,8 @@ 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
<!-- ## Deeper Technical Dive
Let’s break down what happens inside the InteropCenter when a new interop message is created:
Expand All @@ -127,4 +87,4 @@ As you can see, it populates the necessary data and then calls the `sendToL1` me
- 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.
to be determined. -->
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,4 @@ 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.
native **Account Abstraction** model. See [Interop handler](./interop_handler.md) for more details.

0 comments on commit e84c0d8

Please sign in to comment.