This repo explains the design and usage of Unified Bridge and Bridge-and-Call.
Table of Contents
- Unified Bridge & Bridge-and-Call
- Architecture of the Unified Bridge
- Using it as a Developer
- Future TODO
- Reference
As more L2 chains emerge, a key challenge is ecosystem fragmentation. Fragmentation occurs when various L2s operate in silos, each with its own transaction processing, user base, and protocols. This creates several issues:
- Liquidity Fragmentation: Liquidity is spread across multiple L2s, making it challenging to access a unified pool of assets when transacting. Users on one L2 cannot easily interact with assets on another, which reduces overall liquidity effectiveness.
- User Experience (UX) Issues: Users must bridge assets across multiple L2s and adapt to different wallets, transaction formats, and fee structures, leading to a fragmented user experience. This can discourage adoption and reduce ease of access.
- Developer Complexity: Developers must often build custom infrastructure for each L2, which increases development costs and creates inconsistent experiences across L2s. Standardization is difficult, and ensuring cross-L2 compatibility can be resource-intensive.
- Security Risks: Fragmented systems increase potential attack vectors, as users rely on multiple bridges, sequencers, and contracts. Any vulnerability in one L2 can potentially affect assets and users across the ecosystem.
- Coordination Challenges: Governance and decision-making across different L2s can be disjointed, making it hard to create a unified approach to updates, standards, or improvements. This could hinder the collective growth of the ecosystem.
The AggLayer serves as an interoperability layer for cross-chain interactions among the chains connected to it. The AggLayer provides trustless messaging and liquidity sharing across connected chains, making it a more specialized protocol that focuses on asset than general-purpose protocols. However, it is not just a bridge; it's designed to handle native asset transfers and cross-chain execution, providing high security and low latency for connected chains.
The Unified Bridge (Prev. LxLy bridge) is an interoperability layer aimed at enabling cross-chain communication among AggLayer connected chains. It enables the interaction between different networks such as L2 to L2, L1 to L2, and L2 to L1.
Unified Bridge is needed to enable cross-chain communication among different networks to solve the problem of fragmentation as well as to initiate one-step cross-chain transactions for seamless UX.
For AggLayer, it is a critical component to facilitate unified experience among the chains that are connected to AggLayer. In the process of cross-chain communication, Unified Bridge is the interface for developers, users to initiate the cross-chain transactions, then AggLayer will monitor and validate the validity of the cross-chain message via Pessimistic Proof (We will talk about it in another tutorial), then once the validation passes, the cross-chain message will be accepted and ready to be claimed on the destination chain.
The entire data structure of Unified Bridge Looks like this, we will explain each part in detail.
AggLayer is maintaining a giant merkle tree like this to record all the cross-chain transactions, verify the validity of all cross-chain transactions, making sure source chain transaction is indeed finalized on L1 before claiming on the destination chain.
All cross-chain transactions using the Unified Bridge are recorded in a Sparse Merkle Tree called Local Exit Tree. Each AggLayer connected chain maintains its own local exit tree. This is maintained in PolygonZKEVMBridgeV2.sol
on each AggLayer connected L2 and L1.
-
Local Exit Root(LET)
: The root of the tree is called the local exit root. It is a binary tree and has the height of 32. The root is updated every time a new cross-chain transaction is initiated. -
depositCount(Local Root Index)
: The index of the leaf node, per leaf node is a hash of cross-chain transaction such asbridgeAsset
/bridgeMessage
.
rollupExitRoot
is the merkle root of all L2s' Local Exit Root. All AggLayer connected L2s constantly update their Local Exit Root in PolygonRollupManager.sol
, which updates the Rollup Exit Sparse Merkle Tree.
When there's new cross-chain transactions initiated on the Source Chain, is the chain's responsibility to submit its LET to the RollupManager
smart contract on L1. The frequency is up to the L2, whether they submit the new LET for every new cross-chain transaction immediately, or maybe accumulate a certain number of cross-chain transactions before submitting the LET with the number of cross-chain transactions included.
Once the RollupManager has updated a localExitRoot
of an L2, it will then update the rollupExitRoot
on it, which will then update the globalExitRoot
on PolygonZkEVMGlobalExitRootV2.sol
on L1.
mainnetExitRoot
is the same thing as Local Exit Root, but it is maintained on L1, which tracks the Bridging activities of L1 to all AggLayer connected L2s. When Mainnet Exit Root is updated on PolygonZKEVMBridgeV2.sol
contract on L1, it will then update the mainnetExitRoot
on PolygonZkEVMGlobalExitRootV2.sol
on L1.
globalExitRoot
is basically the hash of rollupExitRoot
and mainnetExitRoot
. Whenever there's new RER or MER submitted to PolygonZkEVMGlobalExitRootV2.sol
, it will append the new GER to the L1 Info Tree. L2 syncs L1's latest GER by calling updateExitRoot
function in [PolygonZkEVMGlobalExitRootL2.sol
] on L2.
L1InfoTree
is the Sparse Merkle Tree that maintains the GERs. It is a binary tree and has the height of 32. The root is updated every time a new GER is submitted.
Global Index
to locate the unique leaf in the new global exit tree. It is used when creating and verifying the SMT proof. It is a 256-bit string composed of unused bits, mainnet flag, rollup index bits, and local root index bits. Starting from the most significant bit, consists of the following bits:
- 191 bits of unused bits: These bits are unused, and can be filled with any value. The best option is to fill them with zeros because zeros are cheaper.
- 1 bit of mainnet flag: This single bit serves as a flag indicating whether an exit pertains to a rollup (represented by 0) or the mainnet (indicated by 1).
- 32 bit of the rollup Index: These bits indicate the specific rollup being pointed at, within the rollup exit tree. These bits are therefore only used whenever mainnet flag is 0.
- 32 bits of the local root index: These bits indicate the specific index being pointed at, within each rollup’s local exit tree.
There are two main components of Unified Bridge, the on-chain contracts, and the off-chain services, and additional tools to help interact with Unified Bridge. Let's go through them one by one.
The core of the service that acts as the interface for developers, users to initiate the cross-chain transactions, and facilitate contract calls on the destination chain if specified. It is deployed on both source and destination chains.
Consists of important contracts:
PolygonZKEVMBridgeV2.sol
: Bridge contract on both L1 and L2, maintains its own LET. It is the access point for all cross-chain transactions, includingbridgeAsset
,bridgeMessage
,claimAsset
, andclaimMessage
.PolygonRollupManager.sol
: Rollup Manager contract on L1, all L2 contracts settle on L1 and update their LET via Rollup Manager on L1. Then Rollup Manager updates the RET on L1.PolygonZkEVMGlobalExitRootV2.sol
: Global Exit Root contract on L1 and L2, its root is updated every time when a new Rollup Exit Root or Mainnet Exit Root is updated.- and others.
-
Chain Indexer Framework: An EVM blockchain data indexer. It parses, sorts, and organizes blockchain data for the Bridge Service API. Each chain connected to AggLayer will have its own indexer instance.
-
Transaction API: All details of a bridge transaction initiated by or incoming to a user’s walletAddress. Details include the real time status, the token bridged, the amount, the source and destination chain etc. Used for the user interface to display the status of the transaction.
-
API endpoints are:
-
Testnet:
https://api-gateway.polygon.technology/api/v3/transactions/testnet?userAddress={userAddress}
-
Mainnet:
https://api-gateway.polygon.technology/api/v3/transactions/mainnet?userAddress={userAddress}
-
-
userAddress
should be the address that is associated with the cross-chain transaction. -
Attach API Key in the header! (Check below example to learn more)
-
An example Transaction API can be seen as follows:
curl --location 'https://api-gateway.polygon.technology/api/v3/transactions/mainnet?userAddress={userAddress}' \ --header 'x-api-key: <your-api-key-here>'
API Key: Transaction API & Proof Generation API requires API Key to access. Please Check this guide on how to generate one.
-
-
Proof Generation API: The merkle proof payload needed to process claims on the destination chain.
-
API endpoint are:
- Testnet:
https://api-gateway.polygon.technology/api/v3/proof/testnet/merkle-proof?networkId={sourceNetworkId}&depositCount={depositCount}
- Mainnet:
https://api-gateway.polygon.technology/api/v3/proof/mainnet/merkle-proof?networkId={sourceNetworkId}&depositCount={depositCount}
- Testnet:
-
networkId
is the network ID registered on AggLayer,0
for Ethereum/Sepolia, and1
for Polygon zkEVM/Cardona, and more. -
depositCount
is the leaf index of the Local Exit Tree from the source chain(Explained in the next section). You can get the depositCount by checking thebridgeAsset
/bridgeMessage
event logs or use the Transaction API above to get the depositCount. -
Remember to attach API Key in header!
-
- Claimer: Anyone who wants to help finish the bridging process can become the claimer. Claim Service can be deployed by dapps, chains, or anyone. There's also an auto claiming script available, which automates the claim process on the destination chain.
- Lxly.js: LxLy.js is a javascript library which has all the prebuilt functions for interacting with the lxly bridge contracts. It does most of the heavy lifting, like type conversion, formatting, error handling etc. It makes it very easy for a developer to invoke the bridge, claim and many other functions required for bridging.
There are two types of bridging transactions on Unified Bridge.
- Assets:
bridgeAsset
&claimAsset
are used for bridging tokens from one chain to another. - Message:
bridgeMessage
&claimMessage
are used for bridging messages from one chain to another
BridgeAsset
code can be found here, the interface looks as follows:
/**
* @notice Deposit add a new leaf to the merkle tree
* note If this function is called with a reentrant token, it would be possible to `claimTokens` in the same call
* Reducing the supply of tokens on this contract, and actually locking tokens in the contract.
* Therefore we recommend to third parties bridges that if they do implement reentrant call of `beforeTransfer` of some reentrant tokens
* do not call any external address in that case
* note User/UI must be aware of the existing/available networks when choosing the destination network
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param amount Amount of tokens
* @param token Token address, 0 address is reserved for ether
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
* @param permitData Raw data of the call `permit` of the token
*/
function bridgeAsset(
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
address token,
bool forceUpdateGlobalExitRoot,
bytes calldata permitData
)
The code go through a few steps to complete the bridging process:
- Check the
destinationNetwork
is not set as the source network's ID. - Prepare tokens to be bridged, there are different ways to prepare the tokens to be bridged, depending on the token type:
Token type | Action |
---|---|
Native Gas Token, including ETH and Custom Gas Token | The bridge contract holds the tokens. |
WETH | Burn the WETH tokens from user's address. |
Foreign ERC20 Token | If the token contract is not originally from the source network, burn the ERC20 token from user's address. |
Native ERC20 Token | If the token contract is originally from the source network, run the permitData if provided, then transfer the ERC20 token from user's address to the bridge contract. |
Note that in case
ETH
is the native token, WETHToken will be at0x0
address.
- Emit the
BridgeEvent
- Add the
bridgeAsset
data to theLocal Exit Tree
as a leaf node
claimAsset
code can be found here, the interface looks as follows:
/**
* @notice Verify merkle proof and withdraw tokens/ether
* @param smtProofLocalExitRoot Smt proof to proof the leaf against the network exit root
* @param smtProofRollupExitRoot Smt proof to proof the rollupLocalExitRoot against the rollups exit root
* @param globalIndex Global index is defined as:
* | 191 bits | 1 bit | 32 bits | 32 bits |
* | 0 | mainnetFlag | rollupIndex | localRootIndex |
* note that only the rollup index will be used only in case the mainnet flag is 0
* note that global index do not assert the unused bits to 0.
* This means that when synching the events, the globalIndex must be decoded the same way that in the Smart contract
* to avoid possible synch attacks
* @param mainnetExitRoot Mainnet exit root
* @param rollupExitRoot Rollup exit root
* @param originNetwork Origin network
* @param originTokenAddress Origin token address, 0 address is reserved for ether
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param amount Amount of tokens
* @param metadata Abi encoded metadata if any, empty otherwise
*/
function claimAsset(
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofLocalExitRoot,
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofRollupExitRoot,
uint256 globalIndex,
bytes32 mainnetExitRoot,
bytes32 rollupExitRoot,
uint32 originNetwork,
address originTokenAddress,
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
bytes calldata metadata
)
Before the claimAsset
function is initiated, all these inputs have to be prepared:
- Both
smtProofLocalExitRoot
andsmtProofRollupExitRoot
can be fetched via the Proof Generation API, thedepositCount
param for the Proof API is located at the fetch result of Transaction API, is your bridge transaction'scounter
field in the response. GlobalIndex
can be constructed as described in Global Exit Root, L1 Info Tree, Global Index- The rest can be found via Transaction API.
Then the code will go through a few steps to complete the claiming process:
- Check the
destinationNetwork
is in fact the current network. - Verifies the SMT Proofs.
- Construct the new
globalExitRoot
usingmainnetExitRoot
androllupExitRoot
and compare with the L2 chain's recorded updatedglobalExitRoot
. If they are different then revert. - According to
Global Index
to check if source chain is L1 or L2- if L1 then verify
smtProofLocalExitRoot
withmainnetExitRoot
- if L2 then verify the L2 leaf node with
smtProofLocalExitRoot
,smtProofRollupExitRoot
, androllupExitRoot
- if L1 then verify
- Record that its claimed on the destination network.
- Construct the new
- Once the proof passes, claim the tokens in different ways depending on the token type:
Token type | Action |
---|---|
ETH is gas token | Bridge contract will transfer the amount from itself to the destination address. |
WETH where ETH is not gas token | Mint new WETH to the destination address. |
Custom gas token | Bridge contract will transfer the amount from itself to the destination address. |
Native ERC20 Token | If the token contract is originally from this destination network, the transfer the ERC20 token from bridge contract to destination address. |
Foreign ERC20 Token, First time bridging | Deploy a new ERC20 Token contract to host this new Foreign ERC20 Token. and mint the transfer amount to destination address. |
Foreign ERC20 Token, Contract exist | Mint the transfer amount to destination address. |
- Emit the
ClaimEvent
BridgeMessage
& BridgeMessageWETH
code can be found here, the interface looks as follows:
/**
* @notice Bridge message and send ETH value
* note User/UI must be aware of the existing/available networks when choosing the destination network
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
* @param metadata Message metadata
*/
function bridgeMessage(
uint32 destinationNetwork,
address destinationAddress,
bool forceUpdateGlobalExitRoot,
bytes calldata metadata
) payable
/**
* @notice Bridge message and send ETH value
* note User/UI must be aware of the existing/available networks when choosing the destination network
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param amountWETH Amount of WETH tokens
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
* @param metadata Message metadata
*/
function bridgeMessageWETH(
uint32 destinationNetwork,
address destinationAddress,
uint256 amountWETH,
bool forceUpdateGlobalExitRoot,
bytes calldata metadata
)
The code go through a few steps to complete the bridging process:
- Check value condition:
- [For
BridgeMessage
], if the custom gas token exist in the source chain, if exist, then this function should only callable without value, value should only be transferable via this function if the native gas token isETH
- [For
BridgeMessageWETH
], only allowed to use ifETH
is not the gas token of the chain.
- [For
- Check the
destinationNetwork
is not set as the source network's ID. - Emit the
BridgeEvent
- Add the
bridgeMessage
/bridgeMessageWETH
data to theLocal Exit Tree
as a leaf node.
ClaimMessage
code can be found here, the interface looks as follows:
/**
* @notice Verify merkle proof and execute message
* If the receiving address is an EOA, the call will result as a success
* Which means that the amount of ether will be transferred correctly, but the message
* will not trigger any execution
* @param smtProofLocalExitRoot Smt proof to proof the leaf against the exit root
* @param smtProofRollupExitRoot Smt proof to proof the rollupLocalExitRoot against the rollups exit root
* @param globalIndex Global index is defined as:
* | 191 bits | 1 bit | 32 bits | 32 bits |
* | 0 | mainnetFlag | rollupIndex | localRootIndex |
* note that only the rollup index will be used only in case the mainnet flag is 0
* note that global index do not assert the unused bits to 0.
* This means that when synching the events, the globalIndex must be decoded the same way that in the Smart contract
* to avoid possible synch attacks
* @param mainnetExitRoot Mainnet exit root
* @param rollupExitRoot Rollup exit root
* @param originNetwork Origin network
* @param originAddress Origin address
* @param destinationNetwork Network destination
* @param destinationAddress Address destination
* @param amount message value
* @param metadata Abi encoded metadata if any, empty otherwise
*/
function claimMessage(
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofLocalExitRoot,
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProofRollupExitRoot,
uint256 globalIndex,
bytes32 mainnetExitRoot,
bytes32 rollupExitRoot,
uint32 originNetwork,
address originAddress,
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
bytes calldata metadata
)
The data preparation steps are the same as claimAsset
, the code will go through a few steps to complete the claiming process:
- Check the
destinationNetwork
is in fact the current network. - Verifies the SMT Proofs.
- Construct the new
globalExitRoot
usingmainnetExitRoot
androllupExitRoot
and compare with the L2 chain's recorded updatedglobalExitRoot
. If they are different then revert. - According to
Global Index
to check if source chain is L1 or L2:- if L1 then verify
smtProofLocalExitRoot
withmainnetExitRoot
- if L2 then verify the L2 leaf node with
smtProofLocalExitRoot
,smtProofRollupExitRoot
, androllupExitRoot
- if L1 then verify
- Record that its claimed on the destination network.
- Construct the new
- Once the proof passes, execute the message, (please note message can only be executed if the
destinationAddress
is a smart contract that inherited theIBridgeMessageReceiver.sol
interface).- if the native gas token is
ETH
, then transferETH
to thedestinationAddress
and execute the message. - if
ETH
is not the native gas token, then mintWETH
to thedestinationAddress
and exeute the message.
- if the native gas token is
- Emit the
ClaimEvent
.
Bridge-and-Call is a feature in Unified Bridge that allows developers to initiate a cross-chain transaction call on the destination chain from the source chain.
It is different from Bridge Message, where Bridge Message requires the destination address to be a smart contract that implemented the IBridgeMessageREceiver.sol
interface. Whereas for Bridge-and-Call, it itself is the contract that has implemented the interface, and it will be able to execute any functions on any smart contract in the destination network.
Consist of multiple interface, helper contracts, as well as the extension contract, some of the important ones are:
BridgeExtension.sol
: Bridge Extension contract on both L1 and L2, that access thePolygonZKEVMBridgeV2.sol
contract.JumpPoint.sol
: Process the Destination Chain asset transfer as well as the contract call.
bridgeAndCall
code can be found here, the interface looks as follows:
/**
* @notice Bridge and Call from source chain to destination chain
* @param token Token to send to destination chain
* @param amount Amount of token to send to destination Chain
* @param callAddress The smart contract address to call at the destination network from destination
* BridgeExtension contract.
* @param fallbackAddress If the JumpPoint Execution fails on callAddress, the assets will be transferred to the fallbackAddress on destination network
* @param callData Abi encoded callData if any, empty otherwise
* @param forceUpdateGlobalExitRoot Indicates if the new global exit root is updated or not
*/
function bridgeAndCall(
address token,
uint256 amount,
uint32 destinationNetwork,
address callAddress,
address fallbackAddress,
bytes calldata callData,
bool forceUpdateGlobalExitRoot
) external payable
the code will go through a few steps to complete the bridging process:
- Preparing Bridging Tokens:
- When the source chain's gas token is not
ETH
, then sendWETH
from sender address to the bridge extension contract - When transfering gas token, gas token already transferred to the extension contract
- When transfering any ERC-20 tokens, send the token from sender address to the bridge extension contract
- When the source chain's gas token is not
- Compute JumpPoint address on destination network and call
bridgeAssets
- Check if
bridgeAsset
was successful, if successful then next step. - Encode message to bridge and call
bridgeMessage
onMessageReceived
code can be found here, the interface looks as follows:
/**
* @notice interface for PolygonZkEVM Bridge message receiver
* @param originAddress Message origin sender address. (BridgeExtension Address on source chain)
* @param originNetwork Message origin network. (Source chain network id)
* @param data Abi encoded callData.
*/
function onMessageReceived(
address originAddress,
uint32 originNetwork,
bytes calldata data
) external payable
Note that this function is called when calling claimMessage
on destination network, it's only callable by the Bridge Contract.
-
Access control, making sure its Bridge Contract Calling, and the message origin on the source chain is also bridge contract.
-
Decode
data
intouint256 dependsOnIndex, address callAddress, address fallbackAddress, uint32 assetOriginalNetwork, address assetOriginalAddress, bytes memory callData
-
Check if the Bridge Message is claimed or no
-
Instantiate a new JumpPoint smart contract and execute the asset that was transferred from Bridge Contract to JumpPoint smart contract, transfer them to final
callAddress
contract and executecallData
.
constructor(
address bridge,
uint32 assetOriginalNetwork,
address assetOriginalAddress,
address callAddress,
address fallbackAddress,
bytes memory callData
) payable
The assets transferred from source chain via bridgeAsset
should have already transferred to this new jumpPoint Smart contract on destination network.
- Once instantiated, first thing is to check the asset that was transferred to
this
, whether is aETH
token,WETH
token, Custom Gas Token, or ERC-20 token. - Depending on the token type, transfer the token accordingly to the final
callAddress
, and then do the smart contract call withcallData
- If the execution fails on the
callAddress
contract, tokens are transferred tofallbackAddress
.
-
Secured by Ethereum: Settlement on Ethereum All cross-chain transactions are settled on Ethereum before they can be claimed on the destination chain. This ensures that asset transfers originating from the source chain are valid and secure.
-
Secured by Mathematics: Merkle Proof Validation Once the source chain bridging transaction is finalized on Ethereum, the destination chain verifies the proof to confirm that the assets transferred from the source chain are indeed settled on Ethereum.
-
Secured by Design: Immutable Data Packaging
During an asset transfer from the source chain, the transaction requires the caller to specify the destination chain, the destination address, and the amount of assets to be transferred. These details are bundled together in an immutable cross-chain transaction.
When claiming the assets on the destination chain, the smart contract verifies the correct destination network and processes the claim using the pre-defined destination address and asset amount. This design ensures:
- Assets cannot be claimed on an incorrect network.
- Assets cannot be claimed to an incorrect address.
- Assets cannot be claimed in excess of the transferred amount.
The claim process is open to anyone, as the outcome is predetermined regardless of who initiates the claim. Claimers simply pay the gas fee to facilitate the completion of the cross-chain transaction.
-
Secured by Access Control: No Administrative Privileges The bridge contract can only mint, burn, or transfer assets through user-initiated bridge transactions. There is no administrative control over assets locked in the bridge contract. Only users with a balance of the specific tokens have access to their respective assets.
To learn more about the actions for different types of tokens(gas token, Eth token, ERC-20 Token, etc), please check out the above specs
For more
lxly.js
examples, please refer to the examples folder.
- User/Developer/Dapp initiate
bridgeAsset
call on L1 - Bridge contract on L1 appends an exit leaf to mainnet exit tree of the L1, and update its mainnet exit root.
- Global exit root manager appends the new L1 mainnet exit root to global exit tree and computes the new global exit root.
- L2 sequencer fetches and updates the latest global exit root from the global exit root manager.
- User/Developer/Dapp/Chain initiates
claimAsset
call, and also provides the smtProof. - Bridge contract on destination L2 chain validates the smtProof against the global exit root on its chain. If passes next step.
- Transfer/Mint the asset to the destination address.
We will be using lxly.js
to initiate the bridgeAsset
call and claimAsset
call.
- Check your Balance:
node scripts/src/balance.js
const { getLxLyClient, tokens, configuration, from } = require('./utils/utils_lxly');
const execute = async () => {
// instantiate a lxlyclient
const client = await getLxLyClient();
// Sepolia NetworkId is 0, Cardona NetworkId is 1
const networkId = 0;
// get an api instance of ether token on sepolia testnet
const erc20Token = client.erc20(tokens[networkId].ether, networkId);
// check balance
const result = await erc20Token.getBalance(from);
console.log("result", result);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- Bridge Eth from sepolia to Cardona:
node scripts/src/bridge_asset.js
const { getLxLyClient, tokens, configuration, from, to } = require('./utils/utils_lxly');
const execute = async () => {
// instantiate a lxlyclient
const client = await getLxLyClient();
// source NetworkId is 0, since its Sepolia
const sourceNetworkId = 0;
// get an api instance of ether token on sepolia testnet
const token = client.erc20(tokens[sourceNetworkId].ether, sourceNetworkId);
// Set Destination Network as Cardona
const destinationNetworkId = 1;
// call the `bridgeAsset` api. Bridging 1 eth
const result = await token.bridgeAsset("1000000000000000000", to, destinationNetworkId);
// getting the transactionhash if rpc request is sent
const txHash = await result.getTransactionHash();
console.log("txHash", txHash);
// getting the transaction receipt.
const receipt = await result.getReceipt();
console.log("receipt", receipt);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- Claim Assets after GlobalExitRootManager is synced from source to destination. Since Cardona currently has a autoclaiming bot running, you don't need to do claim asset call. But in case you want to know how to do it, here's the code at
scripts/src/claim_asset.js
:
const { getLxLyClient, tokens, configuration, from } = require('./utils/utils_lxly');
const execute = async () => {
// the source chain txn hash of `bridgeAsset` call.
const bridgeTransactionHash = "";
// instantiate a lxlyclient
const client = await getLxLyClient();
// the source networkId
const sourcenNetworkId = 0;
// the destination networkId
const destinationNetworkId = 1;
// get an api instance of ether token on cardona testnet
const token = client.erc20(tokens[destinationNetworkId].ether, destinationNetworkId);
// call the `claimAsset` api.
const result = await token.claimAsset(bridgeTransactionHash, sourcenNetworkId, {returnTransaction: true});
console.log("result", result);
// getting the transactionhash if rpc request is sent
const txHash = await result.getTransactionHash();
console.log("txHash", txHash);
// getting the transaction receipt.
const receipt = await result.getReceipt();
console.log("receipt", receipt);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- After about 20 mins, your balance of
ETH
on destination chain should have increased to the transfered amount! You can check using thebalance.js
script, but remember to change the network id!
- User/Developer/Dapp initiate
bridgeMessage
call on L2 - Bridge contract on L2 appends an exit leaf to local exit tree of the L2, and update its global exit root on L2.
- Sends the new local exit root to L1 to verify, once passed the L2's local exit root, aka the leaf node in the rollup exit tree will be updated, which will cause a chain of updates to Global exit root updates on L1 and also L1InfoTree updates.
- User/Developer/Dapp/Chain initiates
claimMessage
call, and also provides the smtProof. - Bridge contract on destination L1 chain validates the smtProof against the global exit root on its chain. If passes next step.
- Execute
onMessageReceived
process.
The lxly.js
will be used to initiate the bridgeMessage
call and claimMessage
call.
- Deploy a
onMessageReceived
function implemented smart contractcounter.sol
on destination network, in this example, it will be on Sepolia.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
contract counter {
uint256 public count;
constructor() {
count = 0;
}
function increment(uint256 amount) public {
count = count + amount;
}
// Function to handle the received message
// this is the interface that `claimMessage` will be able to access with.
function onMessageReceived(
address originAddress,
uint32 originNetwork,
bytes calldata metadata
) external payable {
uint256 amount = abi.decode(metadata, (uint256));
require(amount > 0, "Has to increment at least 1");
require(amount < 5, "Has to increment less than 5");
// its also better to check if the msg.sender is the bridge address
// for this demo we will assume it always will be
increment(amount);
}
}
- Bridge Message from cardona to sepolia:
node scripts/src/bridge_message.js
const { getLxLyClient, tokens, configuration, from, to } = require('./utils/utils_lxly');
const { Bridge } = require('@maticnetwork/lxlyjs');
const { encodePacked } = require('viem');
// Encode the amount into a uint256.
function encodeMetadata(amount) {
return encodePacked(["uint256"], [amount]);
}
const execute = async () => {
const client = await getLxLyClient();
// change this with your smart contract deployed on destination network.
const destinationAddress = "0x43854F7B2a37fA13182BBEA76E50FC8e3D298CF1";
// the destination Network ID for this example is spolia, therefore is 0.
const sourceNetworkId = 1;
// Call bridgeMessage function.
const destinationNetworkId = 0;
// the source Network ID for this example is Cardona, therefore is 1.
const result = await client.bridges[sourceNetworkId]
.bridgeMessage(destinationNetworkId, destinationAddress, true, encodeMetadata(3));
const txHash = await result.getTransactionHash();
console.log("txHash", txHash);
const receipt = await result.getReceipt();
console.log("receipt", receipt);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- Claim Message after GlobalExitRootManager is synced on L1.
scripts/src/claim_message.js
:
const { getLxLyClient, tokens, configuration, from, to } = require('./utils/utils_lxly');
const execute = async () => {
const client = await getLxLyClient();
// bridge txn hash from the source chain.
const bridgeTransactionHash = "0xfe25c1d884a7044ba18f6cee886a09a8e94f9ae12c08fd5d94cdc6f430376bf2";
// Network should be set as 1 since its from cardona.
const sourceNetworkId = 1;
// Network should be set as 0 since its to sepolia
const destinationNetworkId = 0;
// API for building payload for claim
const result =
await client.bridgeUtil.buildPayloadForClaim(bridgeTransactionHash, sourceNetworkId)
// payload is then passed to `claimMessage` API
.then((payload) => {
console.log("payload", payload);
return client.bridges[destinationNetworkId].claimMessage(
payload.smtProof,
payload.smtProofRollup,
BigInt(payload.globalIndex),
payload.mainnetExitRoot,
payload.rollupExitRoot,
payload.originNetwork,
payload.originTokenAddress,
payload.destinationNetwork,
payload.destinationAddress,
payload.amount,
payload.metadata
);
});
const txHash = await result.getTransactionHash();
console.log("txHash", txHash);
const receipt = await result.getReceipt();
console.log("receipt", receipt);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- User/Developer/Dapp initiate
bridgeAndCall
call on L2 Source. - Similar to L2 -> L1 process, global exit root on L1 is updated, which includes the source chain bridging transaction.
- Then destination L2 sequencer fetches and updates the latest global exit root from the global exit root manager.
- Bridge contract on destination L2 chain validates the smtProof against the global exit root on its chain. If passes next step.
- Process the
claimAsset
,claimMessage
on destination L2 chain.
TheWe will be using lxly.js will be used to initiate the bridgeAndCall
call and claimMessage
call.
- Deploy the same
counter.sol
contract onzkyoto
testnet. - Call
bridgeAndCall
from cardona to zkyoto:node scripts/src/bridge_and_call.js
const { getLxLyClient, tokens, configuration, from } = require('./utils/utils_lxly');
const { CounterABI } = require("../../ABIs/Counter");
const execute = async () => {
const client = await getLxLyClient();
// set token as `eth`.
const token = "0x0000000000000000000000000000000000000000";
// not bridging any token this time
const amount = "0x0";
// because we are bridging from cardona.
const sourceNetwork = 1;
// sending to zkyoto.
const destinationNetwork = 2;
// change it to the counter smart contract deployed on destination network.
const callAddress = "0x43854F7B2a37fA13182BBEA76E50FC8e3D298CF1";
// if transaction fails, then the funds will be sent back to user's address on destination network.
const fallbackAddress = from;
// if true, then the global exit root will be updated.
const forceUpdateGlobalExitRoot = true;
// get the call Contract ABI instance.
const callContract = client.contract(CounterABI, callAddress, destinationNetwork);
// prepare the call data for the counter smart contract on destination chain.
const callData = await callContract.encodeAbi("increment", "0x4");
let result;
// Call bridgeAndCall function.
if (client.client.network === "testnet") {
console.log("testnet");
result = await client.bridgeExtensions[sourceNetwork].bridgeAndCall(
token,
amount,
destinationNetwork,
callAddress,
fallbackAddress,
callData,
forceUpdateGlobalExitRoot,
permitData="0x0", // permitData is optional
)
} else {
console.log("mainnet");
result = await client.bridgeExtensions[sourceNetwork].bridgeAndCall(
token,
amount,
destinationNetwork,
callAddress,
fallbackAddress,
callData,
forceUpdateGlobalExitRoot,
)
}
console.log("result", result);
const txHash = await result.getTransactionHash();
console.log("txHash", txHash);
const receipt = await result.getReceipt();
console.log("receipt", receipt);
}
execute().then(() => {
}).catch(err => {
console.error("err", err);
}).finally(_ => {
process.exit(0);
});
- Claim Message after GlobalExitRootManager is synced on zkyoto.
scripts/src/claim_message.js
, Remember to updatebridgeTransactionHash
, anddestinationNetworkId
to2
, since zkyoko's network id is 2.
- Asset Bridging
- L1 -> L2(ETH as gas token) Transfer foreign ERC-20
- L1 -> L2(ETH as gas token) Transfer native ERC-20
- L1 -> L2(custom gas token) Transfer L2 custom gas token
- L1 -> L2(custom gas token) Transfer WETH
- L2(ETH as gas token) -> L1 Transfer foreign ERC-20
- L2(ETH as gas token) -> L1 Transfer native ERC-20
- L2(custom gas token) -> L1 Transfer L2 custom gas token
- L2(custom gas token) -> L1 Transfer WETH
- L2(custom gas token) -> L2(custom gas token) transfer WETH
- L2(custom gas token) -> L2(custom gas token) transfer source chain gas token
- L2(custom gas token) -> L2(custom gas token) transfer destination chain gas token
- Message Bridging
- A more complicated
onMessageReceived
custom contract example.
- A more complicated
- Bridge-and-Call Bridging
- L1 -> L2 -> L1
- Defi call example transaction
- https://docs.polygon.technology/zkEVM/architecture/unified-LxLy/
- https://github.com/0xPolygonHermez/zkevm-contracts/tree/main
- https://github.com/agglayer/ulxly-contracts/tree/develop
- https://github.com/agglayer/lxly-bridge-and-call
- https://github.com/0xPolygon/lxly.js
- https://github.com/agglayer/lxly-bridge-and-call-demos
- https://github.com/0xrouss/agglayer-scripts