LayerZero Omnichain Fungible Tokens Adapter
10 Feb 2025 | BlockchainLayerZero
Overview
이전 포스팅에서 LayerZero를 사용하여 ONFT(Omnichain Non-Fungible Tokens)를 배포하고 실행하는 과정을 다뤄보았습니다.
이번 포스팅에선 OFTAdapter(Omnichain Fungible Tokens Adapter)를 배포하고 실행하는 과정을 다뤄보겠습니다.
LayerZero V2 OFTAdapter
ERC20의 safeTransferFrom
을 사용하여 spender로부터 OFT Adapter 컨트랙트로 토큰을 전송하면, 페어링된 OFT 컨트랙트를 통해 선택한 대상 체인(Chain B)에서 동일한 수량의 토큰이 _mint
됩니다.
소스 체인의 OFT Adapter에 있는 토큰을 lock하려면 OFT.send
(Chain B)를 호출해야 하며, 이로 인해 토큰이 _burn
되고 프로토콜을 통해 메시지가 전송되어 Adapter에서 수신 주소(Chain A)로 ERC20.safeTransfer
가 실행됩니다.
Installation
아래 명령어로 OFTAdapter가 포함된 프로젝트를 생성할 수 있습니다.
OFTAdapter
를 선택하면 해당 프로젝트에 OFT가 포함되어 있습니다.
npx create-lz-oapp
이후 아래 명령어를 사용하여 패키지를 추가 설치 해줍니다.
pnpm add @layerzerolabs/oft-evm
Deploy
-
ERC20 Token 컨트랙트 배포
OFTAdapter를 사용하기 위해선 기존 배포되어 있는 ERC20 Token 컨트랙트가 필요합니다.
테스트 환경이기 때문에 직접 배포하여 사용합니다.
-
hardhat.config.ts
구성본문에서는 amoy를 사용하지 않을거라 제거했음
oftAdapter 부분에 0번 과정에서 배포한 tokenAddress를 입력 해 주었음
-
빌드
pnpm install # Install dependencies pnpm compile # Compile contract
.env
구성- Rename
.env.example
->.env
- Choose your preferred means of setting up your deployer wallet/account:
MNEMONIC="test test test test test test test test test test test junk" or... PRIVATE_KEY="0xabc...def"
- Rename
-
배포
npx hardhat lz:deploy
OFTAdapter
설정을 추가해주었기 때문에 sepolia에는 OFTAdapter만 배포 되었습니다. -
layerzero.config.ts
구성실제 컨트랙트를 연결할 체인들을 구성
본문에서는 amoy를 사용하지 않아 제거했음
-
Contract wire
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
- ERC20.approve 수행
- ERC20 토큰에 대해서 OFTAdapter가 컨트롤 할 수 있도록 approve를 수행해줍니다.
- spender는 OFTAdapter 입니다.
-
sendToken 생성 후 실행
// tasks/sendToken.ts import { task } from 'hardhat/config'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { Options } from '@layerzerolabs/lz-v2-utilities'; export default task('sendToken', 'Send a token to the destination chain') .addParam('dstNetwork', 'The destination network name (from hardhat.config.ts)') .addParam('amount', 'The amount of tokens to send') .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { const { amount, dstNetwork } = taskArgs; const [signer] = await hre.ethers.getSigners(); const addressAsBytes32 = "0x0000000000000000000000002f32e86e8fc5e762aa32a09d4970cb3216fefaf4"; // Get destination network's EID const dstNetworkConfig = hre.config.networks[dstNetwork]; const dstEid = dstNetworkConfig.eid; // Get current network's EID const srcNetworkConfig = hre.config.networks[hre.network.name]; const srcEid = srcNetworkConfig?.eid; console.log('Sending message:'); console.log('- From:', signer.address); console.log('- Source network:', hre.network.name, srcEid ? `(EID: ${srcEid})` : ''); console.log('- Destination:', dstNetwork || 'unknown network', `(EID: ${dstEid})`); console.log('- Amount:', amount); const myOFTAdapter = await hre.deployments.get('MyOFTAdapter'); const contract = await hre.ethers.getContractAt('MyOFTAdapter', myOFTAdapter.address, signer); // Add executor options with gas limit const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(); // Get quote for the message console.log('Getting quote...'); const sendParam = { dstEid: dstEid, to: addressAsBytes32, amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: "0x00", oftCmd: "0x00" } const quotedFee = await contract.quoteSend(sendParam, false); console.log('Quoted fee:', hre.ethers.utils.formatEther(quotedFee.nativeFee)); // Send the message console.log('Sending tokens...'); const tx = await contract.send(sendParam, quotedFee, signer.address, {value: quotedFee.nativeFee}); const receipt = await tx.wait(); console.log('🎉 Tokens sent! Transaction hash:', receipt.transactionHash); console.log( 'Check token balance on LayerZero Scan: https://testnet.layerzeroscan.com/tx/' + receipt.transactionHash, ); });
import { EndpointId } from '@layerzerolabs/lz-definitions' import "./tasks/sendToken" // 추가
npx hardhat sendToken --network sepolia-testnet --dst-network avalanche-testnet --amount 100000000000000000
-
아래와 같이 LayerZero Explorer에서도 확인할 수 있음
-
Source Chain에서 처리 됨
-
실제 Fuji chain에 전송된 것을 확인할 수 있음
-
OFTAdapter Setup for Both Chains
위에서는 한쪽은 OFTAdapter, 한쪽은 OFT 컨트랙트를 사용한 예시를 보여드렸습니다.
지금부터는 양쪽에 모두 OFTAdpter를 사용하여 배포한 뒤 실제 전송을 처리해봅니다.
위 과정과 일부 겹치는 부분이 있기 때문에 일부 내용이 생략되었습니다.
Deploy
-
hardhat.config.ts
설정양쪽 체인 모두
oftAdapter
설정 -
빌드
pnpm install # Install dependencies pnpm compile # Compile contract
-
배포
npx hardhat lz:deploy
-
Contract wiring
layerzero.config.ts
에 contractName 확인npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
- ERC20.approve 수행
- ERC20 토큰에 대해서 OFTAdapter가 컨트롤 할 수 있도록 approve를 수행해줍니다.
- spender는 OFTAdapter 입니다.
-
sendToken 생성 후 실행
// tasks/sendToken.ts import { task } from 'hardhat/config'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { Options } from '@layerzerolabs/lz-v2-utilities'; export default task('sendToken', 'Send a token to the destination chain') .addParam('dstNetwork', 'The destination network name (from hardhat.config.ts)') .addParam('amount', 'The amount of tokens to send') .setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => { const { amount, dstNetwork } = taskArgs; const [signer] = await hre.ethers.getSigners(); const addressAsBytes32 = "0x0000000000000000000000002f32e86e8fc5e762aa32a09d4970cb3216fefaf4"; // Get destination network's EID const dstNetworkConfig = hre.config.networks[dstNetwork]; const dstEid = dstNetworkConfig.eid; // Get current network's EID const srcNetworkConfig = hre.config.networks[hre.network.name]; const srcEid = srcNetworkConfig?.eid; console.log('Sending message:'); console.log('- From:', signer.address); console.log('- Source network:', hre.network.name, srcEid ? `(EID: ${srcEid})` : ''); console.log('- Destination:', dstNetwork || 'unknown network', `(EID: ${dstEid})`); console.log('- Amount:', amount); const myOFTAdapter = await hre.deployments.get('MyOFTAdapter'); const contract = await hre.ethers.getContractAt('MyOFTAdapter', myOFTAdapter.address, signer); // Add executor options with gas limit const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toBytes(); // Get quote for the message console.log('Getting quote...'); const sendParam = { dstEid: dstEid, to: addressAsBytes32, amountLD: amount, minAmountLD: amount, extraOptions: options, composeMsg: "0x00", oftCmd: "0x00" } const quotedFee = await contract.quoteSend(sendParam, false); console.log('Quoted fee:', hre.ethers.utils.formatEther(quotedFee.nativeFee)); // Send the message console.log('Sending tokens...'); const tx = await contract.send(sendParam, quotedFee, signer.address, {value: quotedFee.nativeFee}); const receipt = await tx.wait(); console.log('🎉 Tokens sent! Transaction hash:', receipt.transactionHash); console.log( 'Check token balance on LayerZero Scan: https://testnet.layerzeroscan.com/tx/' + receipt.transactionHash, ); });
import { EndpointId } from '@layerzerolabs/lz-definitions' import "./tasks/sendToken" // 추가
npx hardhat sendToken --network sepolia-testnet --dst-network avalanche-testnet --amount 1000000000000000000
-
아래와 같이 LayerZero Explorer에서도 확인할 수 있음
-
Source Chain에서 처리 됨
-
Destination Chain 처리
-