LayerZero V2 + Morph : Transferts de Tokens Cross-Chain Fluides

Daniel KambaleDaniel Kambale
6 min read

LayerZero et le besoin d'une interopérabilité fluide

À mesure que les écosystèmes blockchain continuent de se développer, le besoin de fonctionnalité cross-chain devient de plus en plus crucial. Les actifs et les données existent souvent en isolation sur différentes blockchains, ce qui limite les utilisateurs et les développeurs souhaitant interagir avec plusieurs écosystèmes simultanément. La fonctionnalité cross-chain permet aux actifs, tokens et informations de circuler librement entre les chaînes, améliorant ainsi l'expérience utilisateur et ouvrant de nouvelles possibilités pour les applications décentralisées (dApps).

LayerZero est un protocole d'interopérabilité qui permet aux développeurs de créer des applications (et des tokens) connectables à plusieurs blockchains. LayerZero appelle ces applications des "applications omnichain".

Le protocole LayerZero est composé de Endpoints immuables on-chain, d’une Security Stack configurable, et d’un ensemble de Executors sans autorisation pour transférer des messages entre chaînes.

Bridging d'un Token ERC20 vers Morph avec LayerZero V2

Dans ce tutoriel, nous allons déployer et configurer un OFT (Omnichain Fungible Token).

Les Omnichain Fungible Tokens (OFTs) sont une nouvelle norme de tokens initiée par LayerZero pour les actifs cross-chain. Les OFTs permettent aux tokens fongibles d’être transférés entre différentes blockchains sans avoir besoin de wrapping ou de pools de liquidités, tout en conservant une offre unifiée.

Dans ce guide, vous apprendrez à configurer votre environnement pour les déploiements cross-chain, déployer votre contrat OFT, définir des pairs, et transférer des tokens de Sepolia à Morph Holesky.

Prérequis

  • Pnpm

  • Un portefeuille configuré avec Sepolia et Morph Holesky.

  • Fonds de test :

    • ETH de Sepolia Testnet : Pour déployer des contrats et payer les frais de gas - à réclamer depuis un faucet.

    • ETH de Morph Testnet - Pour déployer des contrats et payer les frais de gas - à réclamer depuis un faucet.

Configuration de votre environnement

Dans votre terminal, créez un nouveau dossier et collez la commande ci-dessous. Nous utiliserons l’outil CLI de LayerZero pour configurer notre dApp.

npx create-lz-oapp@latest

Après avoir exécuté cette commande, vous devriez voir des options pour configurer votre dApp. Pour ce guide, nous utiliserons l'exemple d’OFT.

Pour la première question, qui est de savoir où commencer le projet, appuyez sur “Enter” pour sélectionner l'option par défaut.

Ensuite, sélectionnez OFT comme point de départ, puis choisissez pnpm pour installer les dépendances.

hardhat.config.ts

Après avoir ouvert notre projet dans VSCode, notre première étape sera le fichier de configuration de Hardhat. Dans la section "networks" du fichier de configuration, remplacez les réseaux par ceux ci-dessous :

'sepolia-testnet': {
            eid: EndpointId.SEPOLIA_V2_TESTNET,
            url: 'https://rpc.sepolia.org/',
            accounts,
        },
        'morph-holesky': {
            eid: EndpointId.MORPH_V2_TESTNET,
            url: process.env.RPC_URL_MORPH,
            accounts,
        },

Votre fichier de configuration Hardhat devrait ressembler à ceci.

Pour ce tutoriel, nous allons transférer des jetons (que nous allons créer) de Sepolia vers Morph. Eid fait référence à l'ID du point de terminaison du réseau. Chaque chaîne de la couche zéro possède une adresse de point de terminaison (nous l'utiliserons plus tard) et un ID de point de terminaison. url correspond aux URL respectives des réseaux.

.env

L’étape suivante consiste à configurer les variables d’environnement. Dans le dossier racine de votre projet, renommez le fichier .env.example en .env. Ajoutez et remplissez les variables suivantes :

RPC_URL_MORPH=https://rpc-quicknode-holesky.morphl2.io
PRIVATE_KEY=your-private-key

MyOFT.sol

Il est maintenant temps de travailler sur le contrat lui-même. Nous allons apporter une légère modification au contrat existant en ajoutant la ligne ci-dessous à la fonction du constructeur pour créer 1 000 000 tokens pour le déployeur lors du déploiement.

_mint(msg.sender, 100_000 * 10 ** 18);

Ensuite, nous compilons les contrats pour vérifier que tout fonctionne comme prévu.

pnpm compile:hardhat

Déploiement du contrat OFT

Pour déployer notre contrat OFT, nous devons d'abord initialiser notre fichier layerzero.config.ts. Supprimez le fichier layerzero.config.ts de votre dossier racine et exécutez la commande suivante dans votre terminal :

npx hardhat lz:oapp:config:init --contract-name MyOFT --oapp-config my_oft_config.ts

Cette commande devrait vous présenter deux options, correspondant aux réseaux ajoutés dans le fichier hardhat.config.ts. Sélectionnez les deux chaînes à l’aide de la touche espace et appuyez sur Entrée.

Assurez-vous que le fichier my_oft_config.ts a été créé et qu’il est correctement rempli.

Enfin, pour déployer le contrat, exécutez la commande suivante, sélectionnez à nouveau les deux réseaux, et appuyez sur Entrée.

npx hardhat lz:deploy

L'exécution de la commande de déploiement devrait déployer notre contrat OFT sur les deux réseaux. Vous pouvez consulter les contrats déployés sur Sepolia et Morph holesky explorer.

Configuration des chemins cross-chain

L’étape suivante est de configurer chaque chemin pour notre contrat. Vous pouvez lire plus d’informations sur la configuration des chemins ici. Pour modifier votre contrat LayerZero, exécutez la commande ci-dessous :

npx hardhat lz:oapp:wire --oapp-config my_oft_config.ts

Pour chaque chemin dans votre fichier de configuration, cette tâche appellera les fonctions suivantes :

fromContract.OApp.setPeer fromContract.OApp.setEnforcedOptions fromContract.EndpointV2.setSendLibrary fromContract.EndpointV2.setReceiveLibrary fromContract.EndpointV2.setReceiveLibraryTimeout fromContract.EndpointV2.setConfig(OApp, sendLibrary, sendConfig) fromContract.EndpointV2.setConfig(OApp, receiveLibrary, receiveConfig)

Après l’exécution de cette commande, le fichier my_oft_config.ts devrait maintenant être rempli avec toutes les configurations nécessaires.

Envoi de tokens de Sepolia à Morph

Après avoir configuré le chemin, nous pouvons enfin commencer à transférer des tokens entre chaînes. Créez d’abord un dossier nommé "tasks" dans le répertoire racine de votre projet. À l'intérieur, créez un fichier appelé sendToken.ts. Ensuite, collez le code suivant dans le fichier.

import { ethers } from 'ethers'
import { task } from 'hardhat/config'

import { createGetHreByEid, createProviderFactory, getEidForNetworkName } from '@layerzerolabs/devtools-evm-hardhat'
import { Options } from '@layerzerolabs/lz-v2-utilities'

task('lz:oft:send', 'Send tokens cross-chain using LayerZero technology')
    .addParam('contractA', 'Contract address on network A')
    .addParam('recipientB', 'Recipient address on network B')
    .addParam('networkA', 'Name of the network A')
    .addParam('networkB', 'Name of the network B')
    .addParam('amount', 'Amount to transfer in token decimals')
    .addParam('privateKey', 'Private key of the sender')
    .setAction(async (taskArgs, hre) => {
        const eidA = getEidForNetworkName(taskArgs.networkA)
        const eidB = getEidForNetworkName(taskArgs.networkB)
        const contractA = taskArgs.contractA
        const recipientB = taskArgs.recipientB

        const environmentFactory = createGetHreByEid()
        const providerFactory = createProviderFactory(environmentFactory)
        const provider = await providerFactory(eidA)
        const wallet = new ethers.Wallet(taskArgs.privateKey, provider)

        const oftContractFactory = await hre.ethers.getContractFactory('MyOFT', wallet)
        const oft = oftContractFactory.attach(contractA)

        const decimals = await oft.decimals()
        const amount = hre.ethers.utils.parseUnits(taskArgs.amount, decimals)
        const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString()
        const recipientAddressBytes32 = hre.ethers.utils.hexZeroPad(recipientB, 32)

        // Estimate the fee
        try {
            console.log("Attempting to call quoteSend with parameters:", {
                dstEid: eidB,
                to: recipientAddressBytes32,
                amountLD: amount,
                minAmountLD: amount.mul(98).div(100),
                extraOptions: options,
                composeMsg: '0x',
                oftCmd: '0x',
            });
            const nativeFee = (await oft.quoteSend(
                [eidB, recipientAddressBytes32, amount, amount.mul(98).div(100), options, '0x', '0x'],
                false
            ))[0]
            console.log('Estimated native fee:', nativeFee.toString())

            // Overkill native fee to ensure sufficient gas
            const overkillNativeFee = nativeFee.mul(2)

            // Fetch the current gas price and nonce
            const gasPrice = await provider.getGasPrice()
            const nonce = await provider.getTransactionCount(wallet.address)

            // Prepare send parameters
            const sendParam = [eidB, recipientAddressBytes32, amount, amount.mul(98).div(100), options, '0x', '0x']
            const feeParam = [overkillNativeFee, 0]

            // Sending the tokens with increased gas price
            console.log(`Sending ${taskArgs.amount} token(s) from network ${taskArgs.networkA} to network ${taskArgs.networkB}`)
            const tx = await oft.send(sendParam, feeParam, wallet.address, {
                value: overkillNativeFee,
                gasPrice: gasPrice.mul(2),
                nonce,
                gasLimit: hre.ethers.utils.hexlify(7000000),
            })
            console.log('Transaction hash:', tx.hash)
            await tx.wait()
            console.log(
                `Tokens sent successfully to the recipient on the destination chain. View on LayerZero Scan: https://layerzeroscan.com/tx/${tx.hash}
40
Subscribe to my newsletter

Read articles from Daniel Kambale directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Daniel Kambale
Daniel Kambale

Hello! I’m Daniel, a Web3 developer specializing in Solidity and smart contract development. My journey in the blockchain space is driven by a vision of a fully decentralized world, where technology empowers individuals and transforms industries. As an ambassador at Morph, I’m committed to fostering growth and innovation in this space, helping to shape a future that values transparency and security. Fluent in both English and French, I enjoy connecting with diverse communities and sharing my insights across languages. This is why you’ll find some of my articles in French, while others are in Swahili, as I believe knowledge should be accessible to all. I use my Hashnode blog to document my learning process, explore decentralized solutions, and share practical tutorials on Web3 development. Whether it's diving deep into Solidity, discussing the latest in blockchain, or exploring new tools, I’m passionate about contributing to a decentralized future and connecting with others who share this vision. Let’s build the future together!