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

introduce OFT Fee and OFT Adapter Fee Upgradeable #1283

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
15 changes: 15 additions & 0 deletions examples/oft-upgradeable/contracts/MyOFTAdapterFeeUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { OFTAdapterFeeUpgradeable } from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTAdapterFeeUpgradeable.sol";

contract MyOFTAdapterFeeUpgradeable is OFTAdapterFeeUpgradeable {
constructor(address _token, address _lzEndpoint) OFTAdapterFeeUpgradeable(_token, _lzEndpoint) {
_disableInitializers();
}

function initialize(address _delegate) public initializer {
__OFTAdapterFee_init(_delegate);
__Ownable_init(_delegate);
}
}
15 changes: 15 additions & 0 deletions examples/oft-upgradeable/contracts/MyOFTFeeUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { OFTFeeUpgradeable } from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTFeeUpgradeable.sol";

contract MyOFTFeeUpgradeable is OFTFeeUpgradeable {
constructor(address _lzEndpoint) OFTFeeUpgradeable(_lzEndpoint) {
_disableInitializers();
}

function initialize(string memory _name, string memory _symbol, address _delegate) public initializer {
__OFTFee_init(_name, _symbol, _delegate);
__Ownable_init(_delegate);
}
}
37 changes: 37 additions & 0 deletions examples/oft-upgradeable/deploy/MyOFTAdapterFeeUpgradeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Contract } from 'ethers'
import { type DeployFunction } from 'hardhat-deploy/types'

import { getDeploymentAddressAndAbi } from '@layerzerolabs/lz-evm-sdk-v2'

const contractName = 'MyOFTAdapterFeeUpgradeable'

const deploy: DeployFunction = async (hre) => {
const { deploy } = hre.deployments
const signer = (await hre.ethers.getSigners())[0]
console.log(`deploying ${contractName} on network: ${hre.network.name} with ${signer.address}`)

const { address, abi } = getDeploymentAddressAndAbi(hre.network.name, 'EndpointV2')
const endpointV2Deployment = new Contract(address, abi, signer)

await deploy(contractName, {
from: signer.address,
args: ['0x', endpointV2Deployment.address], // replace '0x' with the address of the ERC-20 token
log: true,
waitConfirmations: 1,
skipIfAlreadyDeployed: false,
proxy: {
proxyContract: 'OpenZeppelinTransparentProxy',
owner: signer.address,
execute: {
init: {
methodName: 'initialize',
args: [signer.address],
},
},
},
})
}

deploy.tags = [contractName]

export default deploy
6 changes: 0 additions & 6 deletions examples/oft-upgradeable/deploy/MyOFTAdapterUpgradeable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ const deploy: DeployFunction = async (hre) => {

const { address, abi } = getDeploymentAddressAndAbi(hre.network.name, 'EndpointV2')
const endpointV2Deployment = new Contract(address, abi, signer)
try {
const proxy = await hre.ethers.getContract('MyOFTUpgradeable')
console.log(`Proxy: ${proxy.address}`)
} catch (e) {
console.log(`Proxy not found`)
}

await deploy(contractName, {
from: signer.address,
Expand Down
37 changes: 37 additions & 0 deletions examples/oft-upgradeable/deploy/MyOFTFeeUpgradeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Contract } from 'ethers'
import { type DeployFunction } from 'hardhat-deploy/types'

import { getDeploymentAddressAndAbi } from '@layerzerolabs/lz-evm-sdk-v2'

const contractName = 'MyOFTFeeUpgradeable'

const deploy: DeployFunction = async (hre) => {
const { deploy } = hre.deployments
const signer = (await hre.ethers.getSigners())[0]
console.log(`deploying ${contractName} on network: ${hre.network.name} with ${signer.address}`)

const { address, abi } = getDeploymentAddressAndAbi(hre.network.name, 'EndpointV2')
const endpointV2Deployment = new Contract(address, abi, signer)

await deploy(contractName, {
from: signer.address,
args: [endpointV2Deployment.address],
log: true,
waitConfirmations: 1,
skipIfAlreadyDeployed: false,
proxy: {
proxyContract: 'OpenZeppelinTransparentProxy',
owner: signer.address,
execute: {
init: {
methodName: 'initialize',
args: ['MyOFT', 'MOFT', signer.address], // TODO: add name/symbol
},
},
},
})
}

deploy.tags = [contractName]

export default deploy
90 changes: 90 additions & 0 deletions packages/oft-evm-upgradeable/contracts/oft/FeeUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-LICENSE-IDENTIFIER: UNLICENSED
pragma solidity ^0.8.20;

import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import { FeeConfig, IFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IFee.sol";

/**
* @title FeeUpgradeable
* @notice Implements fee configuration and calculation.
*/
abstract contract FeeUpgradeable is IFee, Initializable, OwnableUpgradeable {
uint16 public constant BPS_DENOMINATOR = 10_000;

struct FeeStorage {
uint16 defaultFeeBps;
mapping(uint32 => FeeConfig) feeBps;
}

// keccak256(abi.encode(uint256(keccak256("layerzerov2.storage.fee")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant FEE_STORAGE_LOCATION = 0x0cb173d183337e25fab6cb85705c15aad6a58cb1d552ed71b9bc628c8a3de800;

/**
* @dev The init function is intentionally left empty and does not initialize Ownable.
* This is to ensure that Ownable can be initialized in the child contract to accommodate
* potential different versions of Ownable that might be used.
*/
function __Fee_init() internal onlyInitializing {}

function __Fee_init_unchained() internal onlyInitializing {}

function _getFeeStorage() internal pure returns (FeeStorage storage $) {
assembly {
$.slot := FEE_STORAGE_LOCATION
}
}

/**
* @dev Sets the default fee basis points (BPS) for all destinations.
*/
function setDefaultFeeBps(uint16 _feeBps) external onlyOwner {
if (_feeBps > BPS_DENOMINATOR) revert IFee.InvalidBps();
FeeStorage storage $ = _getFeeStorage();
$.defaultFeeBps = _feeBps;
emit DefaultFeeBpsSet(_feeBps);
}

/**
* @dev Sets the fee basis points (BPS) for a specific destination LayerZero EndpointV2 ID.
*/
function setFeeBps(uint32 _dstEid, uint16 _feeBps, bool _enabled) external onlyOwner {
if (_feeBps > BPS_DENOMINATOR) revert IFee.InvalidBps();
FeeStorage storage $ = _getFeeStorage();
$.feeBps[_dstEid] = FeeConfig(_feeBps, _enabled);
emit FeeBpsSet(_dstEid, _feeBps, _enabled);
}

/**
* @dev Returns the fee for a specific destination LayerZero EndpointV2 ID.
*/
function getFee(uint32 _dstEid, uint256 _amount) public view virtual returns (uint256) {
uint16 bps = _getFeeBps(_dstEid);
// @note If amount * bps < BPS_DENOMINATOR, there is no fee
return bps == 0 ? 0 : (_amount * bps) / BPS_DENOMINATOR;
}

function _getFeeBps(uint32 _dstEid) internal view returns (uint16) {
FeeStorage storage $ = _getFeeStorage();
FeeConfig memory config = $.feeBps[_dstEid];
return config.enabled ? config.feeBps : $.defaultFeeBps;
}

/**
* @dev Returns the default fee.
*/
function defaultFeeBps() public view virtual returns (uint16) {
FeeStorage storage $ = _getFeeStorage();
return $.defaultFeeBps;
}

/**
* @dev Returns the configured fee for a given eid.
*/
function feeBps(uint32 _dstEid) public view virtual returns (FeeConfig memory) {
FeeStorage storage $ = _getFeeStorage();
FeeConfig memory config = $.feeBps[_dstEid];
return config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import { IERC20Metadata, IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IOFT, OFTCoreUpgradeable } from "./OFTCoreUpgradeable.sol";
import { FeeUpgradeable } from "./FeeUpgradeable.sol";
import { OFTAdapterUpgradeable } from "./OFTAdapterUpgradeable.sol";

/**
* @title OFTAdapterFeeUpgradeable Contract
* @dev OFTAdapter is a contract that adapts an ERC-20 token to the OFT functionality.
*
* @dev For existing ERC20 tokens, this can be used to convert the token to crosschain compatibility.
* @dev WARNING: ONLY 1 of these should exist for a given global mesh,
* unless you make a NON-default implementation of OFT and needs to be done very carefully.
* @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out.
* IF the 'innerToken' applies something like a transfer fee, the default will NOT work...
* a pre/post balance check will need to be done to calculate the amountSentLD/amountReceivedLD.
*/
abstract contract OFTAdapterFeeUpgradeable is OFTAdapterUpgradeable, FeeUpgradeable {
using SafeERC20 for IERC20;

struct OFTAdapterFeeStorage {
uint256 feeBalance;
}

// keccak256(abi.encode(uint256(keccak256("layerzerov2.storage.oftadapterfee")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OFT_ADAPTER_FEE_STORAGE_LOCATION =
0x75ae803fc5bc34bf9750128bd6622421186811a8566aa9e660cbc56033db8c00;

function _getOFTAdapterFeeStorage() internal pure returns (OFTAdapterFeeStorage storage $) {
assembly {
$.slot := OFT_ADAPTER_FEE_STORAGE_LOCATION
}
}

function feeBalance() public view returns (uint256) {
OFTAdapterFeeStorage storage $ = _getOFTAdapterFeeStorage();
return $.feeBalance;
}

event FeeWithdrawn(address indexed to, uint256 amountLD);

error NoFeesToWithdraw();

/**
* @dev Constructor for initializing the contract with token and endpoint addresses.
* @param _token The address of the token.
* @param _lzEndpoint The address of the LayerZero endpoint.
* @dev _token must implement the IERC20 interface, and include a decimals() function.
*/
constructor(address _token, address _lzEndpoint) OFTAdapterUpgradeable(_token, _lzEndpoint) {}

/**
* @dev Initializes the OFTAdapter with the provided delegate.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*
* @dev The delegate typically should be set as the owner of the contract.
* @dev Ownable is not initialized here on purpose. It should be initialized in the child contract to
* accommodate the different version of Ownable.
*/
function __OFTAdapterFee_init(address _delegate) internal onlyInitializing {
__OFTAdapter_init(_delegate);
__Fee_init();
}

function __OFTAdapterFee_init_unchained() internal onlyInitializing {}

/**
* @notice Withdraws accumulated fees to a specified address.
* @param _to The address to which the fees will be withdrawn.
*/
function withdrawFees(address _to) external onlyOwner {
// @dev doesn't allow owner to pull from the locked assets of the contract,
// only from accumulated fees
OFTAdapterFeeStorage storage $ = _getOFTAdapterFeeStorage();
uint256 balance = $.feeBalance;
if (balance == 0) revert NoFeesToWithdraw();

$.feeBalance = 0;
innerToken.safeTransfer(_to, balance);
emit FeeWithdrawn(_to, balance);
}

/**
* @dev Calculates the amount to be sent and received after applying fees and checking for slippage.
* @param _amountLD The amount of tokens to send in local decimals.
* @param _minAmountLD The minimum amount to send in local decimals.
* @param _dstEid The destination chain ID.
* @return amountSentLD The amount sent in local decimals.
* @return amountReceivedLD The amount received in local decimals on the remote.
*/
function _debitView(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal view virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
amountSentLD = _amountLD;

// @dev Apply the fee, then de-dust the amount afterwards.
// This means the fee is taken from the amount before the dust is removed.
uint256 fee = getFee(_dstEid, _amountLD);
unchecked {
amountReceivedLD = _removeDust(_amountLD - fee);
}

// @dev Check for slippage.
if (amountReceivedLD < _minAmountLD) {
revert SlippageExceeded(amountReceivedLD, _minAmountLD);
}
}

/**
* @dev Transfers the full amount from the sender's balance to the contract,
* then burns the amount minus the fee from the contract leaving the fee locked in the contract.
* @param _from The address to debit from.
* @param _amountLD The amount of tokens to send in local decimals.
* @param _minAmountLD The minimum amount to send in local decimals.
* @param _dstEid The destination chain ID.
* @return amountSentLD The amount sent in local decimals.
* @return amountReceivedLD The amount received in local decimals on the remote.
*/
function _debit(
address _from,
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);

// @dev Lock tokens by moving them into this contract from the caller.
innerToken.safeTransferFrom(_from, address(this), amountSentLD);

if (amountSentLD > amountReceivedLD) {
// @dev Increment the total fees that can be withdrawn.
// Fees include the dust resulting from the de-dust operation.
OFTAdapterFeeStorage storage $ = _getOFTAdapterFeeStorage();
unchecked {
$.feeBalance += (amountSentLD - amountReceivedLD);
}
}
}

/**
* @dev Credits tokens to the specified address.
* @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 returns (uint256 amountReceivedLD) {
if (_to == address(0x0)) _to = address(0xdead);

// @dev Unlock the tokens and transfer to the recipient.
innerToken.safeTransfer(_to, _amountLD);
// @dev In the case of NON-default OFTAdapter, the amountLD MIGHT not be == amountReceivedLD.
return _amountLD;
}
}
Loading
Loading