# Building a Cross-Chain Swap with LayerZero

Cross-chain communication is one of the hottest topics in Web3 today. There are many blockchains in Web3, meaning divided liquidity pools and isolated smart contracts that cannot talk across their blockchain environments. Engineering teams worldwide are trying to deliver a product that allows seamless interchain communication to solve this.

LayerZero is one of these solutions, with an already-functioning product that powers cross-chain bridging on popular decentralized exchanges like [Sushiswap](https://www.sushi.com/swap) and [PancakeSwap](https://bridge.pancakeswap.finance/).

This article will examine LayerZero and how it helps send cross-chain messages.

## How does LayerZero Work?

Any cross-chain communication protocol requires an off-chain entity to send messages to and from. However, this creates a centralization risk.

Here's how LayerZero organizes its' infrastructure:

* LayerZero deployed a smart contract as an endpoint on each supported blockchain. This endpoint serves as a point of assembly for all cross-chain messages. Endpoints are to LayerZero what airports are to air travel.
* An off-chain relayer listens to these endpoints and picks up messages as they arrive. LayerZero lets us [run our own relayer](https://layerzero.gitbook.io/docs/ecosystem/relayer/develop-a-relayer), lowering the risk of centralization.
* However, LayerZero only partially relies on a relayer to deliver these messages. In practice, a relayer works with an oracle that confirms or denies the validity of a transaction.
* The messages are delivered to their destination only if both independent entities agree on the validity of a transaction.
* A relayer is usually paired with decentralized oracle networks like Chainlink to ensure reliability, although, in theory, [we can also develop our own.](https://layerzero.gitbook.io/docs/ecosystem/oracle/develop-an-oracle).
    

[![](https://cdn.hashnode.com/res/hashnode/image/upload/v1692808683942/a9bf7906-d59d-4a44-af98-3fcb15930cbe.jpeg align="center")](https://layerzero.network/pdf/LayerZero_Whitepaper_Release.pdf)

> Note: All of the technical information in this article about LayerZero's working comes [from their whitepaper](https://layerzero.network/pdf/LayerZero_Whitepaper_Release.pdf).

## Target Audience

This guide assumes an understanding of Foundry. If you are uncomfortable with it, [check out the Foundry book](https://book.getfoundry.sh/).

## Prerequisites

This guide requires [an up-to-date installation of Foundry](https://book.getfoundry.sh/getting-started/installation).

## Building a Cross-Chain Swap Between Sepolia and Mumbai Testnets

We will build a cross-chain swap between the Sepolia testnet and the Mumbai testnet that will allow users to swap ETH for MATIC and vice-versa at the click of a button.

[![D_D Newsletter CTA](https://sitemedia.ams3.digitaloceanspaces.com/blog_banner_v1_d1653cce08.png)](https://devdao.to/blog-newsletter-1)

### Creating a New Foundry Project

We create a new directory and open a terminal inside it to get started. To initialize a new Foundry project, we run the following command:
    
```bash
forge init
```

[This LayerZero example repository](https://github.com/LayerZero-Labs/solidity-examples) contains all the interfaces and abstract contracts we need to talk to the on-chain LayerZero endpoints. [We also need the Openzeppelin-contracts library](https://github.com/OpenZeppelin/openzeppelin-contracts) to work with access control in our smart contracts.  

To download these repos into our Foundry project, we run the following:

```bash
forge install LayerZero-Labs/solidity-examples OpenZeppelin/openzeppelin-contracts
```
    
We will work with version `0.8.19` of the Solidity compiler. To ensure Forge uses this exact version to compile all of the Solidity code, we add this line to the `foundry.toml` file in our project directory:
    
```toml
solc_version = '0.8.19'
# The TOML file can be used to configure almost all aspects of
# a Foundry project
```
    
Lastly, since we will be importing Solidity code from different files, we must tell Forge about our remappings. Add this array to the `foundry.toml` file:
    
```toml
remappings = [
    "@layerzero-contracts/=lib/solidity-examples/contracts/",
    "@openzeppelin/=lib/openzeppelin-contracts/"
]
```

### Implementing the Smart Contracts

With our dev environment set up, we are finally ready to begin.

> Disclaimer: This project will be an extremely simplified version of what a cross-chain swap built on top of LayerZero could look like; it is for educational purposes only and should not be considered for any project in production.
> 
> If you want to dive deeper, I recommend you check out [Stargate Finance](https://github.com/stargate-protocol).

Inside the `src` directory, delete `Counter.sol` and create two files new files instead. 

1. `LayerZeroSwap_Mumbai.sol`
2. `LayerZeroSwap_Sepolia.sol`

The first will become the endpoint on the Mumbai testnet and the other on Sepolia.

#### Creating the Base Contract

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@layerzero-contracts/lzApp/NonblockingLzApp.sol";

contract LayerZeroSwap_Mumbai is NonblockingLzApp {
  // the implementation will go here
}
```

`NonblockingLzApp` is an abstract contract built on underlying LayerZero contracts. The purpose of this contract is to make it easier for devs to interact with the on-chain contracts.

#### Adding the Variables

```solidity
    // State variables for the contract    
    uint16 public destChainId;
    bytes payload;
    address payable deployer;
    address payable contractAddress = payable(address(this));

    // Instance of the LayerZero endpoint
    ILayerZeroEndpoint public immutable endpoint;
```
    
The `destChainId` variable represents the **destination** chain's address, not the chain we deploy this contract to. While sending a cross-chain message using LayerZero, we need to specify the address of the intended destination.
    
> Note: These aren't the Chain IDs you might know. To quote the LayerZero docs: "chainId values are not related to EVM IDs. Since LayerZero will span EVM &  non-EVM chains the chainId are proprietary to our Endpoints." Meaning, LayerZero maintains its own set of Chain IDs to identify blockchains that differ from the normally used numbers. [We can find the full reference in their docs](https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses).


`Payload` holds the message we send as bytes. This variable is an ABI-encoded amalgamation of everything we want to send across the chain.
    
We initialize the `deployer` variable in the constructor to the contract owner.
    
The `contractAddress` represents the contract address we know after deployment. We also initialize it in the constructor.
    
The `endpoint` variable is an instance of `ILayerZeroEndpoint` interface; we use it to interact with the on-chain endpoints. [For an example, check out their Mumbai endpoint on Polygonscan](https://mumbai.polygonscan.com/address/0xf69186dfBa60DdB133E91E9A4B5673624293d8F8#code).

#### Implementing the Constructor

```solidity
  constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) {
    deployer = payable(msg.sender);
    endpoint = ILayerZeroEndpoint(_lzEndpoint);

    // If Source == Sepolia, then Destination Chain = Mumbai
    if (_lzEndpoint == 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1)
    destChainId = 10109;

    // If Source == Mumbai, then Destination Chain = Sepolia
    if (_lzEndpoint == 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8)
    destChainId = 10161;
  }
```

We must pass the address of the on-chain endpoint to the `NonblockingLzApp` contract for a successful initialization.  

In this example, we have written two if-statements that automatically assign the Chain ID based on the address of the endpoint contract. If we deployed the contract on Mumbai, the `destChainId` variable will point to Sepolia, and vice-versa.

#### Interlude: How do Smart Contracts Interact with the LayerZero Protocol?

We are now ready to write the two main functions we need to use the LayerZero protocol. But before that, let us take a conceptual detour.

For any smart contract, interacting with the on-chain LayerZero endpoint is a two-part process:

First, we call the `send()` function on the endpoint to send a message. To be clear, this is a function we call on an already deployed contract, not something we define.
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1692814170525/15ae4f0c-a055-444e-a396-09c671b803dd.png align="center")
    
Second, we define the `_nonblockingLzReceive` function in your contract. Any contract that wants to receive cross-chain messages must have this function defined in their contract. The LayerZero endpoint calls this function on our contract to deliver an incoming message. To be clear: **We do not call this function; we just define it!**

#### Implementing the `swapTo_ETH` Function

Let us now define the main swap function. We create a new function named `swapTo_ETH` and define it as follows:

```solidity
function swapTo_ETH(address Receiver) public payable {
    require(msg.value >= 1 ether, "Please send at least 1 MATIC");
    uint value = msg.value;

    bytes memory trustedRemote = trustedRemoteLookup[destChainId];
    require(trustedRemote.length != 0, "LzApp: destination chain is not a trusted source");
    _checkPayloadSize(destChainId, payload.length);

    // The message is encoded as bytes and stored in the "payload" variable.
    payload = abi.encode(Receiver, value);

    endpoint.send{value: 15 ether}(destChainId, trustedRemote, payload, contractAddress, address(0x0), bytes(""));
}
```

First, we ensure the user has sent at least 1 ETH in value while calling the function.
    
A function named `setTrustedRemoteAddress` inside the contract we inherit allows us to designate trusted contracts. This way, we can ensure our contract only interacts with trusted code. `trustedRemoteLookup[destChainId]` returns the endpoint address from the other chain we trust. The `_checkPayloadSize` function ensures our payload size is within acceptable limits.
    
Next, we pack the data we want to send into a single variable of type `bytes` using `abi.encode()`. In our case, we ask the user to tell us the destination address on the other side.
    
Finally, we call the `send()` and transfer 15 ETH from our own smart contract to pay for the gas.
    
> Note: I can already hear you shouting: "15 ETH! What the hell is going on here?"
>  
> We need to pay some gas fees to the endpoint for the execution of our transaction. The actual fee isn't 15 ETH. However, in my experience, transactions that were accompanied by less gas didn't execute.
> 
> While we set 15 ETH in this swap implementation, we get back all unused ETH. 15 ETH is just a buffer to ensure the transaction goes through.
> 
> LayerZero does have functions like `estimateGasFees()` that allow us to estimate the amount of gas that needs to be sent, but I found it to be inaccurate.

#### Implementing the `_nonblockingLzReceive` Function

Well, this is how we **send** a message to the Sepolia testnet, but what if we **receive** a message from there?  

To make sure our contract is ready to handle incoming messages, we need to implement a function named `_nonblockingLzReceive`:

```solidity
function _nonblockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal override {
        
    (address Receiver , uint Value) = abi.decode(_payload, (address, uint));
    address payable recipient = payable(Receiver);        
    recipient.transfer(Value);
}
```

This is the function that LayerZero calls upon our contract to deliver a message. We know what we encoded on the other end. So, we can decode it into a recipient's address and an integer value representing the amount in Wei that we locked into the contract on the other end.
    
Next, we transfer that value to the recipient by calling the `transfer()` function. MATIC and ETH are two very different assets with wildly different values. In any practical implementation of a cross-chain swap, we would use an oracle to coordinate real-time price mediation between the two assets. For this example, we will assume an exchange rate of 1:1.
    
Lastly, we will wrap up the contract code with two simple functions:

```solidity
    // Fallback function to receive ether
    receive() external payable {}

    /**
     * @dev Allows the owner to withdraw all funds from the contract.
     */
    function withdrawAll() external onlyOwner {
        deployer.transfer(address(this).balance);
    }
}
```

#### The Complete Contract for Mumbai

After adding a few comments, this is what the finished `LayerZeroSwap_Mumbai.sol` should look like:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@layerzero-contracts/lzApp/NonblockingLzApp.sol";

/**
 * @title LayerZeroSwap_Mumbai
 * @dev This contract sends a cross-chain message from Mumbai to Sepolia to transfer ETH in return for deposited MATIC.
 */
contract LayerZeroSwap_Mumbai is NonblockingLzApp {

    // State variables for the contract    
    uint16 public destChainId;
    bytes payload;
    address payable deployer;
    address payable contractAddress = payable(address(this));

    // Instance of the LayerZero endpoint
    ILayerZeroEndpoint public immutable endpoint;

    /**
     * @dev Constructor that initializes the contract with the LayerZero endpoint.
     * @param _lzEndpoint Address of the LayerZero endpoint.
     */
    constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) {
        deployer = payable(msg.sender);
        endpoint = ILayerZeroEndpoint(_lzEndpoint);

        // If Source == Sepolia, then Destination Chain = Mumbai
        if (_lzEndpoint == 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1) destChainId = 10109;

        // If Source == Mumbai, then Destination Chain = Sepolia
        if (_lzEndpoint == 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8) destChainId = 10161;
    }

    /**
     * @dev Allows users to swap to ETH.
     * @param Receiver Address of the receiver.
     */
    function swapTo_ETH(address Receiver) public payable {
        require(msg.value >= 1 ether, "Please send at least 1 MATIC");
        uint value = msg.value;

        bytes memory trustedRemote = trustedRemoteLookup[destChainId];
        require(trustedRemote.length != 0, "LzApp: destination chain is not a trusted source");
        _checkPayloadSize(destChainId, payload.length);

        // The message is encoded as bytes and stored in the "payload" variable.
        payload = abi.encode(Receiver, value);

        endpoint.send{value: 15 ether}(destChainId, trustedRemote, payload, contractAddress, address(0x0), bytes(""));
    }

    /**
     * @dev Internal function to handle incoming LayerZero messages.
     */
    function _nonblockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal override {
        
        (address Receiver , uint Value) = abi.decode(_payload, (address, uint));
        address payable recipient = payable(Receiver);        
        recipient.transfer(Value);
    }

    // Fallback function to receive ether
    receive() external payable {}

    /**
     * @dev Allows the owner to withdraw all funds from the contract.
     */
    function withdrawAll() external onlyOwner {
        deployer.transfer(address(this).balance);
    }
}
```

#### The Complete Sepoplia Contract

The Sepolia counterpart of this contract is almost the same, just the other way around.

Now, inside `LayerZeroSwap_Sepolia.sol`, paste the following code:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@layerzero-contracts/lzApp/NonblockingLzApp.sol";

/**
 * @title LayerZeroSwap_Sepolia
 * @dev This contract sends a cross-chain message from Sepolia to Mumbai to transfer MATIC in return for deposited ETH.
 */
contract LayerZeroSwap_Sepolia is NonblockingLzApp {

    // State variables for the contract
    address payable deployer;    
    uint16 public destChainId;
    bytes payload;    
    address payable contractAddress = payable(address(this));

    // Instance of the LayerZero endpoint
    ILayerZeroEndpoint public immutable endpoint;

    /**
     * @dev Constructor that initializes the contract with the LayerZero endpoint.
     * @param _lzEndpoint Address of the LayerZero endpoint.
     */
    constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) {
        deployer = payable(msg.sender);
        endpoint = ILayerZeroEndpoint(_lzEndpoint);

        // If Source == Sepolia, then Destination Chain = Mumbai
        if (_lzEndpoint == 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1) destChainId = 10109;

        // If Source == Mumbai, then Destination Chain = Sepolia
        if (_lzEndpoint == 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8) destChainId = 10161;
    }

    /**
     * @dev Allows users to swap to MATIC.
     * @param Receiver Address of the receiver.
     */
    function swapTo_MATIC(address Receiver) public payable {
        require(msg.value >= 1 ether, "Please send at least 1 ETH");
        uint value = msg.value;

        bytes memory trustedRemote = trustedRemoteLookup[destChainId];
        require(trustedRemote.length != 0, "LzApp: destination chain is not a trusted source");
        _checkPayloadSize(destChainId, payload.length);

        // The message is encoded as bytes and stored in the "payload" variable.
        payload = abi.encode(Receiver, value);

        endpoint.send{value: 15 ether}(destChainId, trustedRemote, payload, contractAddress, address(0x0), bytes(""));
    }

    /**
     * @dev Internal function to handle incoming LayerZero messages.
     */
    function _nonblockingLzReceive(uint16 _srcChainId, bytes memory _srcAddress, uint64 _nonce, bytes memory _payload) internal override {
        (address Receiver , uint Value) = abi.decode(_payload, (address, uint));
        address payable recipient = payable(Receiver);        
        recipient.transfer(Value);
    }

    // Fallback function to receive ether
    receive() external payable {}

    /**
     * @dev Allows the owner to withdraw all funds from the contract.
     */
    function withdrawAll() external onlyOwner {
        deployer.transfer(address(this).balance);
    }
}
```

[![D_D Newsletter CTA](https://sitemedia.ams3.digitaloceanspaces.com/blog_banner_v1_d1653cce08.png)](https://devdao.to/blog-newsletter-1)

### Building the Contracts

We delete the default files inside the `script` and `test` directory and run:

```bash
forge build
```

This command might print some warnings, but we can ignore them.

### Creating the `.env` File

We need to add our sensitive information in a `.env` file to store it securely. So, we create one at the root of our project and fill it with this:

```bash
SEPOLIA_RPC_URL=
MUMBAI_RPC_URL=

PRIVATE_KEY=

ETHERSCAN_API_KEY=
POLYGONSCAN_API_KEY=
```

We can get RPC URLs from services like [**Alchemy**](https://www.alchemy.com/) and [**Chainstack**](https://chainstack.com/), or you could use a public RPC URL. We need 2 RPC URLs since we intend to deploy on two networks. 

We use a private key corresponding to a wallet that holds both ETH and MATIC. Get API keys from [Etherscan](https://etherscan.io/) and [Polygonscan](https://polygonscan.com/). Keys from the mainnet explorers will work on Sepolia and Mumbai as well.
    
With all the values filled, save the `.env` file. Run this command in the terminal to source these variables to the terminal:

```bash
source .env
```

### Implementing the Deployment Scripts

We will deploy our contracts by writing Solidity scripts.  
Create two files inside the script directory, each deploying one contract.

* `Deploy_Sepolia.s.sol`
* `Deploy_Mumbai.s.sol`
    
Inside `Deploy_Sepolia.s.sol`, add the following code:

```solidity
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import {LayerZeroSwap_Sepolia} from "../src/LayerZeroSwap_Sepolia.sol";

contract MyScript is Script {
  LayerZeroSwap_Sepolia layerZeroSwap_Sepolia;

  function run() external {        

    uint256 PrivateKey = vm.envUint("PRIVATE_KEY");
    vm.startBroadcast(PrivateKey);

    layerZeroSwap_Sepolia = new LayerZeroSwap_Sepolia(0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1);

    vm.stopBroadcast();
  }
}
```

To write a deployment script using Foundry, we need to write a contract that inherits from `script.sol`.

First, we declare a variable for the `layerZeroSwap_Sepolia` contract. Then we initialize `layerZeroSwap_Sepolia`, which represents an on-chain deployment. Anything we write between the `startBroadcast()` and `stopBroadcast()`will be executed as an on-chain transaction.
    
We pass the address of the Sepolia endpoint as a constructor argument.
    
Similarly, we paste the following code inside `Deploy_Mumbai.s.sol` for the Mumbai deploy script:

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import {LayerZeroSwap_Mumbai} from "../src/LayerZeroSwap_Mumbai.sol";

contract MyScript is Script {
    LayerZeroSwap_Mumbai layerZeroSwap_Mumbai;

    function run() external {        

        uint256 PrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(PrivateKey);

        layerZeroSwap_Mumbai = new LayerZeroSwap_Mumbai(0xf69186dfBa60DdB133E91E9A4B5673624293d8F8);

        vm.stopBroadcast();
    }
}
```

### Deploying and Verifying the Contracts

To execute `Deploy_Mumbai.s.sol`, we run this command in your terminal:

```bash
forge script script/Deploy_Mumbai.s.sol:DeployMumbai \
--rpc-url $MUMBAI_RPC_URL \
--broadcast -vvvv
```

> Note: We set the verbosity flag (-v) to the maximum of 4. This gives us an entire stack trace of any script we execute in the terminal, which can be very helpful for debugging.

After deploying a contract, Foundry always returns an address. We can use it to verify this contract with the following command:

```bash
forge verify-contract 0x5Bf61Ac6a7B63dDB5D80D125Bc7054916A50d99E \
--chain-id 80001 \
--num-of-optimizations 200 \
--watch --compiler-version v0.8.19+commit.7dd6d404 \
--constructor-args $(cast abi-encode "constructor(address)" 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8)
src/LayerZeroSwap_Mumbai.sol:LayerZeroSwap_Mumbai \
--etherscan-api-key $POLYGONSCAN_API_KEY
```
This is what your terminal should look like right now:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1692825374790/4ebc8851-ff25-4be3-9f1a-3af45d89f0dc.png align="center")

Similarly, we can execute the `Deploy_Sepolia.s.sol` with this command:

```bash
forge script script/Deploy_Sepolia.s.sol:DeploySepolia \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast -vvvv
```

Finally, we can verify the deployments with this command:

```bash
forge verify-contract 0xDA96bbe0B02e64D374bD98355a91405653b081E2 \
--chain-id 11155111 \
--num-of-optimizations 200 \
--watch --compiler-version v0.8.19+commit.7dd6d404 \
--constructor-args $(cast abi-encode "constructor(address)" 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1) \
src/LayerZeroSwap_Sepolia.sol:LayerZeroSwap_Sepolia \
--etherscan-api-key $ETHERSCAN_API_KEY
```

### Connecting the Two Contracts

Remember how we can use the `setTrustedRemoteAddress()` to designate trusted contracts? We need to tell each endpoint of its counterparts so they can call each other.

On Sepolia, we call the function with the following params:  

- `_remoteChainId` = 10109 (LayerZero Chain ID for Mumbai)  
- `_remoteAddress` = Address of our Mumbai contract
    
On Mumbai, call the function with the following params: 
 
- `_remoteChainId` = 10161 (LayerZero Chain ID for Sepolia)  
- `_remoteAddress` = Address of the Sepolia contract
    
Lastly, we ensure to send ETH and MATIC to each contract. Remember, the contracts are the ones paying for gas, so we have to send 30 each to both of the contracts.

## Issues with this Implementation

Before wrapping things up, let's discuss some issues in our cross-chain swap:

* ETH and MATIC are **not** equivalent assets. Any production-ready implementation needs one or more reliable data feeds for the exchange rate.
    
* There is no error handling for failed transactions. If LayerZero fails to deliver a message, we need checks that take over in that scenario.
    
* We did not charge users any fees. In a production implementation, the cross-chain swap must charge some fees to cover their costs.
    
All things considered, this is a very basic implementation, but [the Stargate Finance's Github repo](https://github.com/stargate-protocol/stargate) offers a production-ready solution as a good example.

[![D_D Newsletter CTA](https://sitemedia.ams3.digitaloceanspaces.com/blog_banner_v1_d1653cce08.png)](https://devdao.to/blog-newsletter-1)

## Conclusion

With the robust capabilities of LayerZero, our solution stands ready to facilitate a seamless 1:1 swap between ETH and MATIC.

Should any queries or thoughts arise, don't hesitate to get in touch with me on Twitter. I'm always eager to engage and assist. 

Here's to the boundless possibilities of cross-chain innovations!
