10 Feb 2025 |
Blockchain
LayerZero
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가 포함되어 있습니다.

이후 아래 명령어를 사용하여 패키지를 추가 설치 해줍니다.
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"
-
배포
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

OFTAdapter Setup for Both Chains
위에서는 한쪽은 OFTAdapter, 한쪽은 OFT 컨트랙트를 사용한 예시를 보여드렸습니다.
지금부터는 양쪽에 모두 OFTAdpter를 사용하여 배포한 뒤 실제 전송을 처리해봅니다.
위 과정과 일부 겹치는 부분이 있기 때문에 일부 내용이 생략되었습니다.
Deploy
-
hardhat.config.ts
설정
양쪽 체인 모두 oftAdapter
설정

-
빌드
pnpm install # Install dependencies
pnpm compile # Compile contract
-
배포
-
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

06 Feb 2025 |
Blockchain
LayerZero
Overview
이전 포스팅에서 LayerZero를 사용하여 OApp(Omnichain Applications)과 OFT(Omnichain Fungible Tokens)를 배포하고 실행하는 과정을 다뤄보았습니다.
이번 포스팅에선 ONFT(Omnichain Non-Fungible Tokens)를 배포하고 실행하는 과정을 다뤄보겠습니다.
LayerZero V2 ONFT
ONFT도 OFT와 같이 사용하려는 모든 체인에 ONFT 컨트랙트가 배포 되어 있어야 합니다.
이후 OFT와 동일하게 토큰 전송시 bufn & mint 로직을 사용하고 있습니다.

Installation
아래 명령어로 ONFT가 포함된 프로젝트를 생성할 수 있습니다.
ONFT721
를 선택하면 해당 프로젝트에 OFT가 포함되어 있습니다.

이후 아래 명령어를 사용하여 패키지를 추가 설치 해줍니다.
pnpm add @layerzerolabs/onft-evm
Deploy
-
hardhat.config.ts
구성
본문에서는 amoy를 사용하지 않을거라 제거했음

- Mint function 추가
- 템플릿엔 mint가 별도로 없기 때문에 추가해 주어야 합니다.
- 프로덕션에서 사용하려면
onlyOwner
같은 modifier 를 반드시 넣어주세요
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import { ONFT721 } from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721.sol";
contract MyONFT721 is ONFT721 {
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) ONFT721(_name, _symbol, _lzEndpoint, _delegate) {}
function mint(address to, uint256 tokenId) external virtual {
_mint(to, tokenId);
}
}
-
빌드
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"
-
배포

-
layerzero.config.ts
구성
실제 컨트랙트를 연결할 체인들을 구성
본문에서는 amoy를 사용하지 않아 제거했음

-
Contract wire
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts

- Mint Task 생성 후 실행
-
tasks/mint.ts
// tasks/mint.ts
import { task } from 'hardhat/config';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
export default task('mint', 'Mint a token to the destination chain')
.addParam('id', 'The token id to mint')
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const { id } = taskArgs;
const [signer] = await hre.ethers.getSigners();
console.log('Minting tokens:');
console.log('- From:', signer.address);
console.log('- Token ID:', id);
const myONFT721 = await hre.deployments.get('MyONFT721');
const contract = await hre.ethers.getContractAt('MyONFT721', myONFT721.address, signer);
// Send the message
console.log('Sending message...');
const tx = await contract.mint(signer.address, id);
const receipt = await tx.wait();
console.log('🎉 Tokens minted! Transaction hash:', receipt.transactionHash);
});
-
hardhat.config.ts
에 추가
import { EndpointId } from '@layerzerolabs/lz-definitions'
import "./tasks/mint" // 추가
// Set your preferred authentication method
-
실행
npx hardhat mint --network sepolia-testnet --id 1


-
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('id', 'The token id to send')
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const { id, 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('- Token ID:', id);
const myONFT721 = await hre.deployments.get('MyONFT721');
const contract = await hre.ethers.getContractAt('MyONFT721', myONFT721.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,
tokenId: id,
extraOptions: options,
composeMsg: "0x00",
onftCmd: "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/mint"
import "./tasks/sendToken" // 추가
npx hardhat sendToken --network sepolia-testnet --dst-network avalanche-testnet --id 1

04 Feb 2025 |
Blockchain
LayerZero
Overview
이번 포스팅에서는 LayerZero를 사용하여 OApp(Omnichain Applications)과 OFT(Omnichain Fungible Tokens)를 배포하고 실행하는 과정을 설명합니다.
Getting Started with Contract Standards
LayerZero의 Contract Standards 를 사용하여 임의의 데이터, 토큰 등을 해당 프로토콜을 사용하여 전송할 수 있습니다.
이 표준들은 LayerZero Endpoint contract를 통해 omnichain 메시지를 보내고, 받고, 구성하기 위한 공통 기능을 구현하고 있습니다.
OAppSender._lzSend
: EndpointV2.send
를 호출하여 byte 메시지를 보내는 Internal function
OAppReceiver._lzReceive
:EndpointV2.lzReceive
를 호출한 뒤 encoded message를 byte로 전달하는 internal function
Quickstart - Create Your First Omnichain App
Creating an OApp
아래 명령어를 사용하여 새 프로젝트를 생성합니다.
npx create-lz-oapp@latest
OApp Smart Contract
프로젝트를 생성하면 MyOApp.sol
컨트랙트가 있습니다.
// contracts/MyOApp.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol";
contract MyOApp is OApp {
constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {}
// This is where the message will be stored after it is received on the destination chain
string public data = "Nothing received yet.";
/**
* @notice Sends a message from the source chain to a destination chain.
* @param _dstEid The endpoint ID of the destination chain.
* @param _message The message string to be sent.
* @param _options Additional options for message execution.
* @dev Encodes the message as bytes and sends it using the `_lzSend` internal function.
* @return receipt A `MessagingReceipt` struct containing details of the message sent.
*/
function send(
uint32 _dstEid,
// The message to be sent to the destination chain
string memory _message,
bytes calldata _options
) external payable returns (MessagingReceipt memory receipt) {
bytes memory _payload = abi.encode(_message);
receipt = _lzSend(_dstEid, _payload, _options, MessagingFee(msg.value, 0), payable(msg.sender));
}
/**
* @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token.
* @param _dstEid Destination chain's endpoint ID.
* @param _message The message.
* @param _options Message execution options (e.g., for sending gas to destination).
* @param _payInLzToken Whether to return fee in ZRO token.
* @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token.
*/
function quote(
uint32 _dstEid,
string memory _message,
bytes memory _options,
bool _payInLzToken
) public view returns (MessagingFee memory fee) {
bytes memory payload = abi.encode(_message);
fee = _quote(_dstEid, payload, _options, _payInLzToken);
}
/**
* @dev Internal function override to handle incoming messages from another chain.
* @dev _origin A struct containing information about the message sender.
* @dev _guid A unique global packet identifier for the message.
* @param payload The encoded message payload being received.
*
* @dev The following params are unused in the current implementation of the OApp.
* @dev _executor The address of the Executor responsible for processing the message.
* @dev _extraData Arbitrary data appended by the Executor to the message.
*
* Decodes the received payload and processes it as per the business logic defined in the function.
*/
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata payload,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
data = abi.decode(payload, (string));
}
}
Configuration
hardhat.config.ts
에 배포할 네트워크에 대한 정보를 입력합니다.
networks: {
'avalanche-testnet': {
eid: EndpointId.AVALANCHE_V2_TESTNET,
url: process.env.RPC_URL_FUJI || 'https://rpc.ankr.com/avalanche_fuji',
accounts,
},
'amoy-testnet': {
eid: EndpointId.AMOY_V2_TESTNET,
url: process.env.RPC_URL_AMOY || 'https://polygon-amoy-bor-rpc.publicnode.com',
accounts,
},
}
.env.example
를 복사해 .env
를 만들고 .env
를 수정합니다.
PRIVATE_KEY = your_private_key; // Required
RPC_URL_FUJI = your_fuji_rpc; // Optional but recommended
RPC_URL_AMOY = your_amoy_rpc; // Optional but recommended
Deploying Contracts
아래 명렁어를 이용하여 배포합니다.

Configuration and wiring
이제 Chain간 Contract를 연결할 수 있는 준비가 되었습니다. 이를 위해 설정을 구성해야합니다. 해당 설정은 layerzero.config.ts
에 구성합니다.
// layerzero.config.ts
import {EndpointId} from '@layerzerolabs/lz-definitions';
import type {OAppOmniGraphHardhat, OmniPointHardhat} from '@layerzerolabs/toolbox-hardhat';
const fujiContract: OmniPointHardhat = {
eid: EndpointId.AVALANCHE_V2_TESTNET,
contractName: 'MyOApp',
};
const amoyContract: OmniPointHardhat = {
eid: EndpointId.AMOY_V2_TESTNET,
contractName: 'MyOApp',
};
const config: OAppOmniGraphHardhat = {
contracts: [
{
contract: fujiContract,
},
{
contract: amoyContract,
},
],
connections: [
{
from: fujiContract,
to: amoyContract,
},
{
from: amoyContract,
to: fujiContract,
},
],
};
export default config;
설정을 다 구성 했다면 아래 명령어를 이용하여 contract들을 연결할 수 있습니다.
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts
결과적으로 아래와 같이 출력 됩니다.

Sending Your First Message
이제 실제 메시지를 보내기 위해 ts파일로 script를 구성합니다.
tasks/sendMessage.ts
로 새 파일을 만들고 아래 코드를 입력합니다.
// tasks/sendMessage.ts
import {task} from 'hardhat/config';
import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {Options} from '@layerzerolabs/lz-v2-utilities';
export default task('sendMessage', 'Send a message to the destination chain')
.addParam('dstNetwork', 'The destination network name (from hardhat.config.ts)')
.addParam('message', 'The message to send')
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const {message, dstNetwork} = taskArgs;
const [signer] = await hre.ethers.getSigners();
// 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('- Message:', message);
const myOApp = await hre.deployments.get('MyOApp');
const contract = await hre.ethers.getContractAt('MyOApp', myOApp.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 quotedFee = await contract.quote(dstEid, message, options, false);
console.log('Quoted fee:', hre.ethers.utils.formatEther(quotedFee.nativeFee));
// Send the message
console.log('Sending message...');
const tx = await contract.send(dstEid, message, options, {value: quotedFee.nativeFee});
const receipt = await tx.wait();
console.log('🎉 Message sent! Transaction hash:', receipt.transactionHash);
console.log(
'Check message status on LayerZero Scan: https://testnet.layerzeroscan.com/tx/' +
receipt.transactionHash,
);
});
또한 hardhat.config.ts
에도 위에서 만든 파일을 Import 합니다.
// hardhat.config.ts
// (...)
import {EndpointId} from '@layerzerolabs/lz-definitions';
import './tasks/sendMessage'; // Import the task
이후 아래 명령어로 cross-chain message를 보낼 수 있습니다.
npx hardhat sendMessage --network avalanche-testnet --dst-network amoy-testnet --message "Hello Omnichain World (sent from Avalanche)"
# 실제로는 amoy를 사용하지 않고 sepolia를 사용했음 (Gas fee 없어서)
npx hardhat sendMessage --network sepolia-testnet --dst-network avalanche-testnet --message "Hello Omnichain World (sent from Seploia)"
결과는 아래와 같습니다.
Sending message:
- From: 0x498098ca1b7447fC5035f95B80be97eE16F82597
- Source network: avalanche-testnet (EID: 40106)
- Destination: amoy-testnet (EID: 40267)
- Message: Hello Omnichain World (sent from Avalanche)
Getting quote...
Quoted fee: 0.004605311339306711
Sending message...
🎉 Message sent! Transaction hash: 0x47bd60f2710c2ec5a496c55c9763bd87fd4c599b541ad1287540fce9852ede65
Check message status on LayerzeRo Scan: https://testnet.layerzeroscan.com/tx/0x47bd60f2710c2ec5a496c55c9763bd87fd4c599b541ad1287540fce9852ede65
-
실패 ( avax → sepolia )



LayerZero V2 OFT

OFT는 모든 체인에 배포되어야 하며, 각 체인에서 토큰을 발행하고 전송할 수 있습니다.
각 체인간 전송하는 과정에서 기본적으로 Burn & Mint 처리가 됩니다.

만약 이미 존재하는 ERC20을 사용하여 OFTAdapter를 구성한 경우 Lock & Mint 처리가 됩니다.
Installation
아래 명령어로 OFT가 포함된 프로젝트를 생성할 수 있습니다.

추가로 아래 명령어로 npm 패키지를 설치합니다.
pnpm add @layerzerolabs/oft-evm
OFT를 생성하려면, 토큰을 존재시키고자 하는 모든 체인에 OFT Contract를 배포해야 합니다.
이미 해당 체인에 토큰이 존재하는 경우, 해당 토큰의 중개 역할을 하는 Lock Box로서 OFT Adapter Contract를 배포할 수 있습니다.
기본적으로 OFT는 ERC20 규칙을 따르며, decimals
값을 18
로 설정합니다. 다른 값을 사용하려면 컨트랙트에서 decimals() 함수를 오버라이드해야 합니다.
Deployment Workflow
-
OFT를 연결하려는 모든 체인에 배포
OFT를 생성하려면, 토큰을 존재시키고자 하는 모든 체인에 OFT 컨트랙트를 배포해야 합니다.
-
setPeer를 사용하여 체인 간 연결
OFT는 OApp을 확장하므로, OFT.setPeer를 호출하여 각 대상 체인의 컨트랙트를 허용 목록(whitelist)에 추가해야 합니다.
-
DVN(Decentralized Verification Network) 설정
블록 확인(block confirmations), 보안 임계값(security threshold), 실행자(Executor), 최대 메시지 크기(max message size), 송·수신 라이브러리(send/receive libraries) 등의 설정을 구성해야 합니다.
-
보안 및 실행자(Executor) 설정 스크립트 참고
보안 및 실행자 설정과 관련된 예제 스크립트는 “Security and Executor Configuration” 섹션에서 확인할 수 있습니다.
-
가스 설정 (권장 옵션)
OFT는 OAppOptionsType3을 상속받으므로, 사용자가 aOFT.send를 호출할 때 특정 가스 설정을 강제할 수 있습니다.
-
OFTAdapter를 사용하는 경우 (필수 설정)
OFTAdapter를 사용할 경우, 전송할 토큰의 양만큼 ERC20.approve를 호출하여 OFTAdapter가 ERC20 토큰을 사용할 수 있도록 승인해야 합니다.
Deploy
-
hardhat.config.ts
구성
본문에서는 amoy를 사용하지 않을거라 제거했음

- Mint function 추가
- 템플릿엔 mint가 별도로 없기 때문에 추가해 주어야 합니다.
- 프로덕션에서 사용하려면
onlyOwner
같은 modifier 를 반드시 넣어주세요
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
contract MyOFT is OFT {
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}
// Mint function
function mint(address to, uint256 amount) external virtual {
_mint(to, amount);
}
}
-
빌드
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"
-
배포

-
layerzero.config.ts
구성
실제 컨트랙트를 연결할 체인들을 구성
본문에서는 amoy를 사용하지 않아 제거했음

-
Contract wire
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts

- Mint Task 생성 후 실행
-
tasks/mint.ts
// tasks/mint.ts
import { task } from 'hardhat/config';
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { Options } from '@layerzerolabs/lz-v2-utilities';
export default task('mint', 'Mint a token to the destination chain')
.addParam('amount', 'The amount of tokens to mint')
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const { amount } = taskArgs;
const [signer] = await hre.ethers.getSigners();
console.log('Minting tokens:');
console.log('- From:', signer.address);
console.log('- Amount:', amount);
const myOFT = await hre.deployments.get('MyOFT');
const contract = await hre.ethers.getContractAt('MyOFT', myOFT.address, signer);
// Send the message
console.log('Sending message...');
const tx = await contract.mint(signer.address, amount);
const receipt = await tx.wait();
console.log('🎉 Tokens minted! Transaction hash:', receipt.transactionHash);
});
-
hardhat.config.ts
에 추가
import { EndpointId } from '@layerzerolabs/lz-definitions'
import "./tasks/mint" // 추가
// Set your preferred authentication method
-
실행
npx hardhat mint --network sepolia-testnet --amount 10000000000000000000


-
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 myOFT = await hre.deployments.get('MyOFT');
const contract = await hre.ethers.getContractAt('MyOFT', myOFT.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/mint"
import "./tasks/sendToken" // 추가
npx hardhat sendToken --network sepolia-testnet --dst-network avalanche-testnet --amount 1000000000000000000

-
아래와 같이 LayerZero Explorer에서도 확인할 수 있음

-
실제 Fuji chain에 전송된 것을 확인할 수 있음

-
Source Chain에서는 send와 함께 Burn 처리됨

TroubleShooting
-
Message Blocked

- Config이 잘못되는 경우 Blocked가 되는 경우가 있음
create-lz-oapp
을 사용해서 만드는 프로젝트의 기본설정을 사용했는데 그게 문제가 된 것 같음
layerzero.config.ts
의 connections 부분을 수정
connections: [
{
from: fujiContract,
to: sepoliaContract,
},
{
from: sepoliaContract,
to: fujiContract,
}
],
25 Dec 2024 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 4에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Treasure hunt
실제 challenge의 내용은 보물찾기를 통해 찾아야 합니다.
시작점은 다음 엔드포인트입니다
curl https://treasurehunt.fermyon.app/start

단서를 토대로 아래 명령어를 통해서 POST 요청을 보냈습니다.
curl -X POST https://treasurehunt.fermyon.app/artist-check \
-H "Content-Type: application/json" \
-d '{"name": "Bryan Adams"}'

다음 힌트를 얻기 위해 GET /song-hint
엔드포인트로 요청을 보냅니다
curl https://treasurehunt.fermyon.app/song-hint

이 base64로 인코딩된 텍스트를 디코딩해보면 노래 가사를 알 수 있을 것 같습니다. 터미널에서 다음 명령어로 디코딩해봅니다.
echo "U2FpZCBTYW50YSB0byBhIGJveSBjaGlsZCAiV2hhdCBoYXZlIHlvdSBiZWVuIGxvbmdpbmcgZm9yPyIKIkFsbCBJIHdhbnQgZm9yIENocmlzdG1hcyBpcyBhIFJvY2sgYW5kIFJvbGwgZWxlY3RyaWMgZ3VpdGFyIgpBbmQgYXdheSB3ZW50IFJ1ZG9scGggYSB3aGl6emluZyBsaWtlIGEgc2hvb3Rpbmcgc3Rhci4K" | base64 -d

이 가사는 Run Rudolph Run의 일부입니다.
아래 명령어로 답을 제출해봅니다.
curl -X POST https://treasurehunt.fermyon.app/song-check \
-H "Content-Type: application/json" \
-d '{"name": "Run Rudolph Run"}'

아래 명령어로 마지막 문제를 확인합니다.
curl https://treasurehunt.fermyon.app/final-riddle

아래 명령어로 정답을 확인합니다.
curl -X POST https://treasurehunt.fermyon.app/answer \
-H "Content-Type: application/json" \
-d '{"name": "Dasher"}'

이제 실제 구현 spec이 나왔습니다.
Spec
GET /api/my
엔드포인트 생성
content-type
헤더는 application/json
이어야 함
-
아래 포맷을 만족해야함
{
"favorite" : {
"music": "YOUR_LINK_HERE"
}
}
Work
- 이번 챌린지는 단순하게
GET
메소드로 라우팅을 하나만 뚫으면 되는 간단한 챌린지 입니다.
-
spin new
명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.

-
/api/my
라우팅을 지정하기 위해 spin.toml
을 수정합니다.

로 수정 해 주었습니다.
-
src/lib.rs
를 구현하기 위해 아래처럼 코드를 작성합니다.
use spin_sdk::http::{IntoResponse, Method, Request, Response};
use spin_sdk::http_component;
#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
println!("Received request: {:?}", req.method());
let (status, body) = match *req.method() {
Method::Get => {
let json_body = serde_json::json!({
"favorite": {
"music": "https://music.apple.com/de/playlist/advent-of-spin-2024/pl.u-xky2TDpLaZ?l=en-GB"
}
});
(200, serde_json::to_vec(&json_body).unwrap())
}
_ => (404, Vec::new()),
};
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
- serde_json 을 dependency로 사용하기 때문에
cargo add serde_json
을 수행하여 의존성 추가를 해 주어야 합니다.
Test
16 Dec 2024 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 3에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Spec
- Gift-Suggestion-Generator Wasm 컴포넌트
/gift-suggestions.html
페이지 추가
- 사용자 이름, 나이, 취향을 입력하면 선물 제안을 생성하는 API를 호출하는 페이지
/api/generate-gift-suggestions
path에서 POST method 구현하기’
- AI가 선물 제안을 생성하는 API
- request
{
"name": "Riley Parker",
"age": 15,
"likes": "Computers, Programming, Mechanical Keyboards"
}
- response
{
"name": "Riley Parker",
"giftSuggestions": "I bet Riley would be super happy with a new low profile mechanical keyboard or a couple of new books about software engineering"
}
Work
이 Challenge도 Challenge 2과 마찬가지로 static page와 api가 구현된 wasm component를 사용하기 위해 challenge 2에서 만들어놓은 프로젝트를 기반으로 사용하려고 합니다.
우선 프론트엔드 부분을 구현하기 위해 assets/gift-suggestions.html
파일을 생성합니다.
assets/gift-suggestions.html
경로에 GET 요청을 보내면 아래와 같은 응답을 받을 수 있도록 구현합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gift Suggestions</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f9f9f9;
}
h1 {
color: #2c3e50;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
margin-bottom: 20px;
}
label {
font-weight: bold;
}
input, button {
padding: 10px;
font-size: 1rem;
}
button {
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #2980b9;
}
#result {
background-color: white;
border: 1px solid #ddd;
padding: 15px;
max-width: 400px;
}
</style>
</head>
<body>
<h1>🎁 Gift Suggestions</h1>
<form id="giftForm">
<label for="name">Name:</label>
<input type="text" id="name" placeholder="Enter name" required>
<label for="age">Age:</label>
<input type="number" id="age" placeholder="Enter age" required>
<label for="likes">Likes/Interests:</label>
<input type="text" id="likes" placeholder="e.g., Programming, Sports" required>
<button type="submit">Get Gift Suggestions</button>
</form>
<div id="result" hidden>
<h2>Gift Suggestions for <span id="childName"></span></h2>
<p id="suggestions"></p>
</div>
<script>
const form = document.getElementById("giftForm");
const resultDiv = document.getElementById("result");
const childNameSpan = document.getElementById("childName");
const suggestionsParagraph = document.getElementById("suggestions");
form.addEventListener("submit", async (event) => {
event.preventDefault();
// 폼에서 데이터 가져오기
const name = document.getElementById("name").value;
const age = parseInt(document.getElementById("age").value);
const likes = document.getElementById("likes").value;
// API 요청
try {
const response = await fetch("/api/generate-gift-suggestions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, age, likes })
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
const data = await response.json();
// 결과 표시
childNameSpan.textContent = name;
suggestionsParagraph.textContent = data.giftSuggestions;
resultDiv.hidden = false;
} catch (error) {
console.error("Error fetching gift suggestions:", error);
alert("Failed to fetch gift suggestions. Please try again later.");
}
});
</script>
</body>
</html>


이번에도 Component Dependencies
기능을 사용하여 선물 아이디어를 반환하도록 하는 기능이 포함된 wasm component를 추가해보겠습니다.
이 컴포넌트는 gIthub 의 ./template
폴더에 포함되어 있으므로 해당 파일을 가져와서 사용하겠습니다.

첫번째로 venv를 사용하여 가상 환경을 구성합니다.
그러면 venv
폴더가 생성됩니다.

이제 가상 환경을 활성화합니다.

이후에 Dependencies를 설치합니다.
pip install -r requirements.txt

이제 wasm component를 구현 해야합니다. app.py
파일을 확인하면 아래와 같은 코드를 확인할 수 있습니다.
from gift_suggestions_generator import exports
from gift_suggestions_generator.exports.generator import Suggestions
# from spin_sdk import llm
class Generator(exports.Generator):
def suggest(self, name: str, age: int, likes: str):
# Implement your gift suggestion here
return Suggestions("John Doe", "I bet John would be super happy with a new mechanical keyboard.")
우리는 이 코드를 llm을 이용하여 더 적절하게 추천을 할 수 있도록 수정해보겠습니다.
from gift_suggestions_generator import exports
from gift_suggestions_generator.exports.generator import Suggestions
from spin_sdk import llm
class Generator(exports.Generator):
def suggest(self, name: str, age: int, likes: str):
prompt = (
f"Suggest a personalized gift for {name}, "
f"a {age}-year-old person who likes {likes}. "
"Be creative and thoughtful, and provide a brief reason for your suggestion."
)
try:
result = llm.infer("llama2-chat", prompt)
suggestion_text = result.text
except Exception as e:
suggestion_text = "Unable to generate a suggestion at the moment. Please try again later."
return Suggestions(name, suggestion_text)
이제 wasm component를 생성하기 위해 아래 커맨드를 입력합니다.
componentize-py -d ./wit/ -w gift-suggestions-generator componentize -m spin_sdk=spin-imports app -o gift-suggestions-generator.wasm

그러면 gift-suggestions-generator.wasm
파일이 생성됩니다.

이제 이 wasm 파일을 spin 프로젝트에 추가해야 합니다.
spin deps add ./gift-suggestions-generator.wasm
커맨드를 사용하여 spin 프로젝트에 추가해 줍니다.


이후 spin deps generate-bindings
커맨드를 사용하여 wasm 파일에 대한 binding을 생성해 주어야 하는데 이를 위해서 spin plugin을 설치해 주어야 합니다.
- https://github.com/fermyon/spin-deps-plugin/tree/main?tab=readme-ov-file#installation
설치가 완료 되었다면 아래 명령어를 사용하여 binding을 생성합니다.
spin deps generate-bindings -L rust -o src/bindings -c challenge3
위 과정이 정상적으로 수행 되엇다면 src/bindings
경로에 파일이 생성된 것을 확인할 수 있습니다.


이제 이 파일을 이용하여 rust 코드를 작성해보겠습니다.
src/lib.rs
에 GET을 처리하기 위한 로직을 구현합니다.
use serde::{Deserialize, Serialize};
use spin_sdk::http::Method;
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
mod bindings;
#[derive(Debug, Serialize, Deserialize)]
struct SuggestionsRequest {
name: String,
age: u8,
likes: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SuggestionsResonse {
name: String,
#[warn(non_snake_case)]
giftSuggestions: String,
}
#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
println!("Received request: {:?}", req.method());
let (status, body) = match *req.method() {
Method::Post => {
let body = req.body();
let suggestions_request: SuggestionsRequest = match serde_json::from_slice(&body) {
Ok(suggestions_request) => suggestions_request,
Err(_) => return Ok(Response::builder().status(400).body(Vec::new()).build()),
};
let gift_suggestions = bindings::deps::components::advent_of_spin::generator::suggest(
&suggestions_request.name,
suggestions_request.age,
&suggestions_request.likes,
)
.unwrap();
let json_body = SuggestionsResonse {
name: gift_suggestions.clone().name,
giftSuggestions: gift_suggestions.clone().suggestions,
};
(200, serde_json::to_vec(&json_body).unwrap())
}
_ => (404, Vec::new()),
};
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
실행 전 spin.toml
에 해당 옵션을 추가해 줍니다.
dependencies_inherit_configuration = true
ai_models = ["llama2-chat"]
