Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layerzero OFT #2

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions zen-oft-v2/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# general parameters
PRIVATE_KEY=

OTHER_CHAIN_PRIVATE_KEY=
OTHER_CHAIN_RPC=https://sepolia-rollup.arbitrum.io/rpc

CHAIN_LZ_ID=40306
OTHER_CHAIN_LZ_ID=40231

# deploy multichain parameters
CHAIN_TOKEN_NAME=ZEN
CHAIN_TOKEN_SYMBOL=ZEN
CHAIN_LZ_ENDPOINT=0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff
OTHER_CHAIN_LZ_ENDPOINT=0x6EDCE65403992e310A62460808c4b910D972f10f

OTHER_CHAIN_TOKEN_NAME=LZZEN
OTHER_CHAIN_TOKEN_SYMBOL=LZZEN

# demo transfer parameters
CHAIN_TOKEN_ADDRESS=0x742ae08ab454b3022C6E5f90c53073c2B8741feB
OTHER_CHAIN_TOKEN_ADDRESS=0x916352E3b55D517bB9Dd74F7734761E9292eAC6c
AMOUNT=100
17 changes: 17 additions & 0 deletions zen-oft-v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
node_modules
.env

# Hardhat files
/cache
/artifacts

# TypeChain files
/typechain
/typechain-types

# solidity-coverage files
/coverage
/coverage.json

# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-31337
19 changes: 19 additions & 0 deletions zen-oft-v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ZEN OFT

LayerZero V2 files (`contracts/oapp`, `contracts/oft` and `contracts/precrime`) are downloaded from the [official GitHub repository](https://github.com/LayerZero-Labs/LayerZero-v2), branch **main**, commit **943ce4a**, on 2024-11-29 10:44 CET

## Build
`npm install`
`npx hardhat compile`

## Deploy on two chains
Config *.env* file with required parameters, then launch

`npx hardhat run ./scripts/deploy-multichain.ts`


## Demo transfer
Config *.env* file with required parameters, then launch

`npx hardhat run ./scripts/demo-transfer.ts`

37 changes: 37 additions & 0 deletions zen-oft-v2/contracts/OtherSideOFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 OtherSideOFT is OFT {

error FailedUnwrap();
error NotEnoughMsgValue();

constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}

function decimals() public view virtual override returns (uint8) {
return 18;
}

/**
* @dev Retrieves the shared decimals of the OFT.
* @return The shared decimals of the OFT.
*
* @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap
* Lowest common decimal denominator between chains.
* Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64).
* For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller.
* ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615
*/
function sharedDecimals() public view virtual override returns (uint8) {
return 18;
}
}
171 changes: 171 additions & 0 deletions zen-oft-v2/contracts/ZenNativeOFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
import { SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { MessagingParams, MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";


contract ZenNativeOFT is OFT, ReentrancyGuard {

uint256 immutable public CONVERSION_RATE;

error FailedUnwrap();
error NotEnoughMsgValue();

constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {
CONVERSION_RATE = 10**(18-decimals());
}

function decimals() public view virtual override returns (uint8) {
return 18;
}

/**
* @dev Retrieves the shared decimals of the OFT.
* @return The shared decimals of the OFT.
*
* @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap
* Lowest common decimal denominator between chains.
* Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64).
* For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller.
* ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615
*/
function sharedDecimals() public view virtual override returns (uint8) {
return 18;
}

/**
* @dev Executes the send operation.
* @param _sendParam The parameters for the send operation.
* @param _fee The calculated fee for the send() operation.
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
* @param _refundAddress The address to receive any excess funds.
* @return msgReceipt The receipt for the send operation.
* @return oftReceipt The OFT receipt information.
*
* @dev MessagingReceipt: LayerZero msg receipt
* - guid: The unique identifier for the sent message.
* - nonce: The nonce of the sent message.
* - fee: The LayerZero fee incurred for the message.
*/

function send(
SendParam calldata _sendParam,
MessagingFee calldata _fee,
address _refundAddress
) external payable virtual override returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious why we are overriding some of these functions if they are inherited from LayerZero's OFT contract

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, it's because we invoke the custom _lzSendWithAmount instead of _lsZend
I can't simply override the _lzSend`` because I had to add the parameter amount, that it was not explicitely inserted before. Same for _payNative, the original one doesn't check that msg.value` is amount + fee because the original isn't developed for auto-wrapping native tokens.

Similar for _credit, it is overriden since it has to send native token instead of minting erc20 token

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. just curious why we need to pass the amount explicitly and not use msg.sender?

// - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender.
// - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance.
(uint256 amountSentLD, uint256 amountReceivedLD) = _debitView(_sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid);

// @dev Builds the options and OFT message to quote in the endpoint.
(bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD);

// @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt.
msgReceipt = _lzSendWithAmount(_sendParam.dstEid, amountSentLD, message, options, _fee, _refundAddress);
// @dev Formulate the OFT receipt.
oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD);

emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD);
}

/**
* @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message.
* @param _dstEid The destination endpoint ID.
* @param _message The message payload.
* @param _options Additional options for the message.
* @param _fee The calculated LayerZero fee for the message.
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
* @param _refundAddress The address to receive any excess fee values sent to the endpoint.
* @return receipt The receipt for the sent message.
* - guid: The unique identifier for the sent message.
* - nonce: The nonce of the sent message.
* - fee: The LayerZero fee incurred for the message.
*/
function _lzSendWithAmount(
uint32 _dstEid,
uint256 _amount,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal virtual returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
uint256 messageValue = _payNative(_amount, _fee.nativeFee);
if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee);

return
// solhint-disable-next-line check-send-result
endpoint.send{ value: messageValue }(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
}

/**
* @dev Internal function to pay the native fee associated with the message.
* @param _nativeFee The native fee to be paid.
* @return nativeFee The amount of native currency paid.
*
* @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction,
* this will need to be overridden because msg.value would contain multiple lzFees.
* @dev Should be overridden in the event the LayerZero endpoint requires a different native currency.
* @dev Some EVMs use an ERC20 as a method for paying transactions/gasFees.
* @dev The endpoint is EITHER/OR, ie. it will NOT support both types of native payment at a time.
*/
function _payNative(uint256 amount, uint256 _nativeFee) internal virtual returns (uint256 nativeFee) {
if (msg.value != amount + _nativeFee) revert NotEnoughMsgValue();
return _nativeFee;
}

/**
* @dev Internal function to mock the amount mutation from a OFT debit() operation.
* @param _amountLD The amount to send in local decimals.
* @param _minAmountLD The minimum amount to send in local decimals.
* @dev _dstEid The destination endpoint ID.
* @return amountSentLD The amount sent, in local decimals.
* @return amountReceivedLD The amount to be received on the remote chain, in local decimals.
*
* @dev This is where things like fees would be calculated and deducted from the amount to be received on the remote.
*/
function _debitView(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 /*_dstEid*/
) internal view virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
if (_amountLD < _minAmountLD) {
revert SlippageExceeded(_amountLD, _minAmountLD);
}
return (_amountLD, _amountLD);
}


// ----------------------------- RECEIVER PART
/**
* @dev Unwrap tokens
* @param _to The address to credit the tokens to.
* @param _amountLD The amount of tokens to credit in local decimals.
* @dev _srcEid The source chain ID.
* @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
*/
function _credit(
address _to,
uint256 _amountLD,
uint32 /*_srcEid*/
) internal virtual override nonReentrant returns (uint256 amountReceivedLD) {
(bool success, ) = _to.call{value: _amountLD * CONVERSION_RATE}("");
if(!success) revert FailedUnwrap();

return _amountLD;
}
}
55 changes: 55 additions & 0 deletions zen-oft-v2/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomiclabs/hardhat-ethers";
import * as dotenv from "dotenv";

dotenv.config();

const account = [process.env.PRIVATE_KEY!]

const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 1,
}
}
},
]
},
defaultNetwork: "hardhat",
networks: {
hardhat: {
gasPrice: 0,
initialBaseFeePerGas: 0,
mining: {
auto: true
}
},
pregobi: {
url: "http://evm-tn-pre-gobi-test-1.de.horizenlabs.io/ethv1",
accounts: account
},
gobi: {
url: "https://rpc.ankr.com/horizen_gobi_testnet",
accounts: account
},
eon: {
url: "https://eon-rpc.horizenlabs.io/ethv1",
accounts: account
},
amoy: {
url: "https://rpc-amoy.polygon.technology",
accounts: account
},
curtis: {
url: "https://rpc.curtis.apechain.com",
accounts: account
}
}
};

export default config;
Loading