Skip to content

Commit

Permalink
Merge pull request #16 from LayerZero-Labs/riley/oft
Browse files Browse the repository at this point in the history
chore: add updated OFT contracts and tests
  • Loading branch information
carmenjiawenc authored Jan 31, 2024
2 parents 026844e + 1289df1 commit 5f82f1c
Show file tree
Hide file tree
Showing 25 changed files with 1,729 additions and 22 deletions.
8 changes: 4 additions & 4 deletions oapp/contracts/oapp/OApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

pragma solidity ^0.8.20;

// @dev Import the 'MessagingFee' so it's exposed to OApp implementers
// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers
// solhint-disable-next-line no-unused-import
import { OAppSender, MessagingFee } from "./OAppSender.sol";
import { OAppSender, MessagingFee, MessagingReceipt } from "./OAppSender.sol";
// @dev Import the 'Origin' so it's exposed to OApp implementers
// solhint-disable-next-line no-unused-import
import { OAppReceiver, Origin } from "./OAppReceiver.sol";
Expand All @@ -18,9 +18,9 @@ abstract contract OApp is OAppSender, OAppReceiver {
/**
* @dev Constructor to initialize the OApp with the provided endpoint and owner.
* @param _endpoint The address of the LOCAL LayerZero endpoint.
* @param _owner The address of the owner of the OApp.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(address _endpoint, address _owner) OAppCore(_endpoint, _owner) {}
constructor(address _endpoint, address _delegate) OAppCore(_endpoint, _delegate) {}

/**
* @notice Retrieves the OApp version information.
Expand Down
14 changes: 8 additions & 6 deletions oapp/contracts/oapp/OAppCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ abstract contract OAppCore is IOAppCore, Ownable {
mapping(uint32 eid => bytes32 peer) public peers;

/**
* @dev Constructor to initialize the OAppCore with the provided endpoint and owner.
* @dev Constructor to initialize the OAppCore with the provided endpoint and delegate.
* @param _endpoint The address of the LOCAL Layer Zero endpoint.
* @param _owner The address of the owner of the OAppCore.
* @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.
*/
constructor(address _endpoint, address _owner) {
_transferOwnership(_owner);
constructor(address _endpoint, address _delegate) {
endpoint = ILayerZeroEndpointV2(_endpoint);
endpoint.setDelegate(_owner); // @dev By default, the owner is the delegate

if (_delegate == address(0)) revert InvalidDelegate();
endpoint.setDelegate(_delegate);
}

/**
Expand Down Expand Up @@ -60,7 +63,6 @@ abstract contract OAppCore is IOAppCore, Ownable {
*
* @dev Only the owner/admin of the OApp can call this function.
* @dev Provides the ability for a delegate to set configs, on behalf of the OApp, directly on the Endpoint contract.
* @dev Defaults to the owner of the OApp.
*/
function setDelegate(address _delegate) public onlyOwner {
endpoint.setDelegate(_delegate);
Expand Down
17 changes: 14 additions & 3 deletions oapp/contracts/oapp/OAppReceiver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

pragma solidity ^0.8.20;

import { ILayerZeroReceiver, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol";
import { IOAppReceiver, Origin } from "./interfaces/IOAppReceiver.sol";
import { OAppCore } from "./OAppCore.sol";

/**
* @title OAppReceiver
* @dev Abstract contract implementing the ILayerZeroReceiver interface and extending OAppCore for OApp receivers.
*/
abstract contract OAppReceiver is ILayerZeroReceiver, OAppCore {
abstract contract OAppReceiver is IOAppReceiver, OAppCore {
// Custom error message for when the caller is not the registered endpoint/
error OnlyEndpoint(address addr);

Expand All @@ -23,13 +23,24 @@ abstract contract OAppReceiver is ILayerZeroReceiver, OAppCore {
* @return receiverVersion The version of the OAppReceiver.sol contract.
*
* @dev Providing 0 as the default for OAppSender version. Indicates that the OAppSender is not implemented.
* ie. this is a SEND only OApp.
* ie. this is a RECEIVE only OApp.
* @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions.
*/
function oAppVersion() public view virtual returns (uint64 senderVersion, uint64 receiverVersion) {
return (0, RECEIVER_VERSION);
}

/**
* @notice Retrieves the address responsible for 'sending' composeMsg's to the Endpoint.
* @return sender The address responsible for 'sending' composeMsg's to the Endpoint.
*
* @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer.
* @dev The default sender IS the OApp implementer.
*/
function composeMsgSender() public view virtual returns (address sender) {
return address(this);
}

/**
* @notice Checks if the path initialization is allowed based on the provided origin.
* @param origin The origin information containing the source endpoint and sender address.
Expand Down
2 changes: 1 addition & 1 deletion oapp/contracts/oapp/OAppSender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ abstract contract OAppSender is OAppCore {
* @return receiverVersion The version of the OAppReceiver.sol contract.
*
* @dev Providing 0 as the default for OAppReceiver version. Indicates that the OAppReceiver is not implemented.
* ie. this is a RECEIVE only OApp.
* ie. this is a SEND only OApp.
* @dev If the OApp uses both OAppSender and OAppReceiver, then this needs to be override returning the correct versions
*/
function oAppVersion() public view virtual returns (uint64 senderVersion, uint64 receiverVersion) {
Expand Down
2 changes: 1 addition & 1 deletion oapp/contracts/oapp/examples/OmniCounter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ contract OmniCounter is ILayerZeroComposer, OApp, OAppPreCrimeSimulator {
mapping(uint32 srcEid => uint256 count) public inboundCount;
mapping(uint32 dstEid => uint256 count) public outboundCount;

constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {
constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) {
admin = msg.sender;
eid = ILayerZeroEndpointV2(_endpoint).eid();
}
Expand Down
2 changes: 1 addition & 1 deletion oapp/contracts/oapp/examples/OmniCounterPreCrime.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contract OmniCounterPreCrime is PreCrime {
uint256 outboundCount;
}

constructor(address _endpoint, address _counter, address _owner) PreCrime(_endpoint, _counter, _owner) {}
constructor(address _endpoint, address _counter) PreCrime(_endpoint, _counter) {}

function buildSimulationResult() external view override returns (bytes memory) {
address payable payableSimulator = payable(simulator);
Expand Down
1 change: 1 addition & 0 deletions oapp/contracts/oapp/interfaces/IOAppCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface IOAppCore {
error OnlyPeer(uint32 eid, bytes32 sender);
error NoPeer(uint32 eid);
error InvalidEndpointCall();
error InvalidDelegate();

// Event emitted when a peer (OApp) is set for a corresponding endpoint
event PeerSet(uint32 eid, bytes32 peer);
Expand Down
15 changes: 15 additions & 0 deletions oapp/contracts/oapp/interfaces/IOAppReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { ILayerZeroReceiver, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol";

interface IOAppReceiver is ILayerZeroReceiver {
/**
* @notice Retrieves the address responsible for 'sending' composeMsg's to the Endpoint.
* @return sender The address responsible for 'sending' composeMsg's to the Endpoint.
*
* @dev Applications can optionally choose to implement a separate composeMsg sender that is NOT the bridging layer.
* @dev The default sender IS the OApp implementer.
*/
function composeMsgSender() external view returns (address sender);
}
100 changes: 100 additions & 0 deletions oapp/contracts/oft/OFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IOFT, OFTCore } from "./OFTCore.sol";

/**
* @title OFT Contract
* @dev OFT is an ERC-20 token that extends the functionality of the OFTCore contract.
*/
abstract contract OFT is OFTCore, ERC20 {
/**
* @dev Constructor for the OFT contract.
* @param _name The name of the OFT.
* @param _symbol The symbol of the OFT.
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) ERC20(_name, _symbol) OFTCore(decimals(), _lzEndpoint, _delegate) {}

/**
* @notice Retrieves interfaceID and the version of the OFT.
* @return interfaceId The interface ID.
* @return version The version.
*
* @dev interfaceId: This specific interface ID is '0x02e49c2c'.
* @dev version: Indicates a cross-chain compatible msg encoding with other OFTs.
* @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented.
* ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1)
*/
function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) {
return (type(IOFT).interfaceId, 1);
}

/**
* @dev Retrieves the address of the underlying ERC20 implementation.
* @return The address of the OFT token.
*
* @dev In the case of OFT, address(this) and erc20 are the same contract.
*/
function token() external view returns (address) {
return address(this);
}

/**
* @notice Indicates whether the OFT contract requires approval of the 'token()' to send.
* @return requiresApproval Needs approval of the underlying token implementation.
*
* @dev In the case of OFT where the contract IS the token, approval is NOT required.
*/
function approvalRequired() external pure virtual returns (bool) {
return false;
}

/**
* @dev Burns tokens from the sender's specified balance.
* @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(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);

// @dev In NON-default OFT, amountSentLD could be 100, with a 10% fee, the amountReceivedLD amount is 90,
// therefore amountSentLD CAN differ from amountReceivedLD.

// @dev Default OFT burns on src.
_burn(msg.sender, amountSentLD);
}

/**
* @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) {
// @dev Default OFT mints on dst.
_mint(_to, _amountLD);
// @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD.
return _amountLD;
}
}
118 changes: 118 additions & 0 deletions oapp/contracts/oft/OFTAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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, OFTCore } from "./OFTCore.sol";

/**
* @title OFTAdapter 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 OFTAdapter is OFTCore {
using SafeERC20 for IERC20;

IERC20 internal immutable innerToken;

/**
* @dev Constructor for the OFTAdapter contract.
* @param _token The address of the ERC-20 token to be adapted.
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(
address _token,
address _lzEndpoint,
address _delegate
) OFTCore(IERC20Metadata(_token).decimals(), _lzEndpoint, _delegate) {
innerToken = IERC20(_token);
}

/**
* @notice Retrieves interfaceID and the version of the OFT.
* @return interfaceId The interface ID.
* @return version The version.
*
* @dev interfaceId: This specific interface ID is '0x02e49c2c'.
* @dev version: Indicates a cross-chain compatible msg encoding with other OFTs.
* @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented.
* ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1)
*/
function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) {
return (type(IOFT).interfaceId, 1);
}

/**
* @dev Retrieves the address of the underlying ERC20 implementation.
* @return The address of the adapted ERC-20 token.
*
* @dev In the case of OFTAdapter, address(this) and erc20 are NOT the same contract.
*/
function token() external view returns (address) {
return address(innerToken);
}

/**
* @notice Indicates whether the OFT contract requires approval of the 'token()' to send.
* @return requiresApproval Needs approval of the underlying token implementation.
*
* @dev In the case of default OFTAdapter, approval is required.
* @dev In non-default OFTAdapter contracts with something like mint and burn privileges, it would NOT need approval.
*/
function approvalRequired() external pure virtual returns (bool) {
return true;
}

/**
* @dev Burns tokens from the sender's specified balance, ie. pull method.
* @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.
*
* @dev msg.sender will need to approve this _amountLD of tokens to be locked inside of the contract.
* @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 amountReceivedLD.
*/
function _debit(
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(msg.sender, address(this), amountSentLD);
}

/**
* @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.
*
* @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 amountReceivedLD.
*/
function _credit(
address _to,
uint256 _amountLD,
uint32 /*_srcEid*/
) internal virtual override returns (uint256 amountReceivedLD) {
// @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

0 comments on commit 5f82f1c

Please sign in to comment.