diff --git a/contracts/GHST/GHSTOFT/GHSTAdapter.sol b/contracts/GHST/GHSTOFT/GHSTAdapter.sol new file mode 100644 index 000000000..9508d090f --- /dev/null +++ b/contracts/GHST/GHSTOFT/GHSTAdapter.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import "./oft/OFTAdapter.sol"; + +contract GHSTOFTAdapter is OFTAdapter { + constructor( + address _token, // a deployed, already existing ERC20 token address + address _layerZeroEndpoint, // local endpoint address + address _owner // token owner used as a delegate in LayerZero Endpoint + ) OFTAdapter(_token, _layerZeroEndpoint, _owner) {} +} diff --git a/contracts/GHST/GHSTOFT/GHSTOFT.sol b/contracts/GHST/GHSTOFT/GHSTOFT.sol new file mode 100644 index 000000000..42d7c2a94 --- /dev/null +++ b/contracts/GHST/GHSTOFT/GHSTOFT.sol @@ -0,0 +1,11 @@ +import {OFT} from "./oft/OFT.sol"; + +pragma solidity ^0.8.20; + +/////////////////////////////////////////////// +// This contract must be deployed to the DESTINATION chain +/////////////////////////////////////////////// + +contract GHSTOFT is OFT { + constructor(string memory _name, string memory _symbol, address _lzEndpoint, address _delegate) OFT(_name, _symbol, _lzEndpoint, _delegate) {} +} diff --git a/contracts/GHST/GHSTOFT/oapp/OApp.sol b/contracts/GHST/GHSTOFT/oapp/OApp.sol new file mode 100644 index 000000000..b58d1a61a --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/OApp.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +// @dev Import the 'MessagingFee' and 'MessagingReceipt' so it's exposed to OApp implementers +// solhint-disable-next-line no-unused-import +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"; +import { OAppCore } from "./OAppCore.sol"; + +/** + * @title OApp + * @dev Abstract contract serving as the base for OApp implementation, combining OAppSender and OAppReceiver functionality. + */ +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 _delegate The delegate capable of making OApp configurations inside of the endpoint. + */ + constructor(address _endpoint, address _delegate) OAppCore(_endpoint, _delegate) {} + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol implementation. + * @return receiverVersion The version of the OAppReceiver.sol implementation. + */ + function oAppVersion() + public + pure + virtual + override(OAppSender, OAppReceiver) + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, RECEIVER_VERSION); + } +} diff --git a/contracts/GHST/GHSTOFT/oapp/OAppCore.sol b/contracts/GHST/GHSTOFT/oapp/OAppCore.sol new file mode 100644 index 000000000..966e5d542 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/OAppCore.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IOAppCore, ILayerZeroEndpointV2 } from "./interfaces/IOAppCore.sol"; + +/** + * @title OAppCore + * @dev Abstract contract implementing the IOAppCore interface with basic OApp configurations. + */ +abstract contract OAppCore is IOAppCore, Ownable { + // The LayerZero endpoint associated with the given OApp + ILayerZeroEndpointV2 public immutable endpoint; + + // Mapping to store peers associated with corresponding endpoints + mapping(uint32 eid => bytes32 peer) public peers; + + /** + * @dev Constructor to initialize the OAppCore with the provided endpoint and delegate. + * @param _endpoint The address of the LOCAL Layer Zero endpoint. + * @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 _delegate) { + endpoint = ILayerZeroEndpointV2(_endpoint); + + if (_delegate == address(0)) revert InvalidDelegate(); + endpoint.setDelegate(_delegate); + } + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Indicates that the peer is trusted to send LayerZero messages to this OApp. + * @dev Set this to bytes32(0) to remove the peer address. + * @dev Peer is a bytes32 to accommodate non-evm chains. + */ + function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { + peers[_eid] = _peer; + emit PeerSet(_eid, _peer); + } + + /** + * @notice Internal function to get the peer address associated with a specific endpoint; reverts if NOT set. + * ie. the peer is set to bytes32(0). + * @param _eid The endpoint ID. + * @return peer The address of the peer associated with the specified endpoint. + */ + function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { + bytes32 peer = peers[_eid]; + if (peer == bytes32(0)) revert NoPeer(_eid); + return peer; + } + + /** + * @notice Sets the delegate address for the OApp. + * @param _delegate The address of the delegate to be set. + * + * @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. + */ + function setDelegate(address _delegate) public onlyOwner { + endpoint.setDelegate(_delegate); + } +} diff --git a/contracts/GHST/GHSTOFT/oapp/OAppReceiver.sol b/contracts/GHST/GHSTOFT/oapp/OAppReceiver.sol new file mode 100644 index 000000000..c797f6f93 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/OAppReceiver.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +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 IOAppReceiver, OAppCore { + // Custom error message for when the caller is not the registered endpoint/ + error OnlyEndpoint(address addr); + + // @dev The version of the OAppReceiver implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant RECEIVER_VERSION = 1; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @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 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. + * @return Whether the path has been initialized. + * + * @dev This indicates to the endpoint that the OApp has enabled msgs for this particular path to be received. + * @dev This defaults to assuming if a peer has been set, its initialized. + * Can be overridden by the OApp if there is other logic to determine this. + */ + function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { + return peers[origin.srcEid] == origin.sender; + } + + /** + * @notice Retrieves the next nonce for a given source endpoint and sender address. + * @dev _srcEid The source endpoint ID. + * @dev _sender The sender address. + * @return nonce The next nonce. + * + * @dev The path nonce starts from 1. If 0 is returned it means that there is NO nonce ordered enforcement. + * @dev Is required by the off-chain executor to determine the OApp expects msg execution is ordered. + * @dev This is also enforced by the OApp. + * @dev By default this is NOT enabled. ie. nextNonce is hardcoded to return 0. + */ + function nextNonce(uint32 /*_srcEid*/, bytes32 /*_sender*/) public view virtual returns (uint64 nonce) { + return 0; + } + + /** + * @dev Entry point for receiving messages or packets from the endpoint. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _guid The unique identifier for the received LayerZero message. + * @param _message The payload of the received message. + * @param _executor The address of the executor for the received message. + * @param _extraData Additional arbitrary data provided by the corresponding executor. + * + * @dev Entry point for receiving msg/packet from the LayerZero endpoint. + */ + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) public payable virtual { + // Ensures that only the endpoint can attempt to lzReceive() messages to this OApp. + if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); + + // Ensure that the sender matches the expected peer for the source endpoint. + if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) revert OnlyPeer(_origin.srcEid, _origin.sender); + + // Call the internal OApp implementation of lzReceive. + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + /** + * @dev Internal function to implement lzReceive logic without needing to copy the basic parameter validation. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; +} diff --git a/contracts/GHST/GHSTOFT/oapp/OAppSender.sol b/contracts/GHST/GHSTOFT/oapp/OAppSender.sol new file mode 100644 index 000000000..891f34c3a --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/OAppSender.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MessagingParams, MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { OAppCore } from "./OAppCore.sol"; + +/** + * @title OAppSender + * @dev Abstract contract implementing the OAppSender functionality for sending messages to a LayerZero endpoint. + */ +abstract contract OAppSender is OAppCore { + using SafeERC20 for IERC20; + + // Custom error messages + error NotEnoughNative(uint256 msgValue); + error LzTokenUnavailable(); + + // @dev The version of the OAppSender implementation. + // @dev Version is bumped when changes are made to this contract. + uint64 internal constant SENDER_VERSION = 1; + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @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 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) { + return (SENDER_VERSION, 0); + } + + /** + * @dev Internal function to interact with the LayerZero EndpointV2.quote() for fee calculation. + * @param _dstEid The destination endpoint ID. + * @param _message The message payload. + * @param _options Additional options for the message. + * @param _payInLzToken Flag indicating whether to pay the fee in LZ tokens. + * @return fee The calculated MessagingFee for the message. + * - nativeFee: The native fee for the message. + * - lzTokenFee: The LZ token fee for the message. + */ + function _quote( + uint32 _dstEid, + bytes memory _message, + bytes memory _options, + bool _payInLzToken + ) internal view virtual returns (MessagingFee memory fee) { + return + endpoint.quote( + MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _payInLzToken), + address(this) + ); + } + + /** + * @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 _lzSend( + uint32 _dstEid, + 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(_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 _nativeFee) internal virtual returns (uint256 nativeFee) { + if (msg.value != _nativeFee) revert NotEnoughNative(msg.value); + return _nativeFee; + } + + /** + * @dev Internal function to pay the LZ token fee associated with the message. + * @param _lzTokenFee The LZ token fee to be paid. + * + * @dev If the caller is trying to pay in the specified lzToken, then the lzTokenFee is passed to the endpoint. + * @dev Any excess sent, is passed back to the specified _refundAddress in the _lzSend(). + */ + function _payLzToken(uint256 _lzTokenFee) internal virtual { + // @dev Cannot cache the token because it is not immutable in the endpoint. + address lzToken = endpoint.lzToken(); + if (lzToken == address(0)) revert LzTokenUnavailable(); + + // Pay LZ token fee by sending tokens to the endpoint. + IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); + } +} diff --git a/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppComposer.sol b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppComposer.sol new file mode 100644 index 000000000..d6c038631 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppComposer.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; + +/** + * @title IOAppComposer + * @dev This interface defines the OApp Composer, allowing developers to inherit only the OApp package without the protocol. + */ +// solhint-disable-next-line no-empty-blocks +interface IOAppComposer is ILayerZeroComposer {} diff --git a/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppCore.sol b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppCore.sol new file mode 100644 index 000000000..ad7af41e8 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppCore.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +/** + * @title IOAppCore + */ +interface IOAppCore { + // Custom error messages + 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); + + /** + * @notice Retrieves the OApp version information. + * @return senderVersion The version of the OAppSender.sol contract. + * @return receiverVersion The version of the OAppReceiver.sol contract. + */ + function oAppVersion() external view returns (uint64 senderVersion, uint64 receiverVersion); + + /** + * @notice Retrieves the LayerZero endpoint associated with the OApp. + * @return iEndpoint The LayerZero endpoint as an interface. + */ + function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); + + /** + * @notice Retrieves the peer (OApp) associated with a corresponding endpoint. + * @param _eid The endpoint ID. + * @return peer The peer address (OApp instance) associated with the corresponding endpoint. + */ + function peers(uint32 _eid) external view returns (bytes32 peer); + + /** + * @notice Sets the peer address (OApp instance) for a corresponding endpoint. + * @param _eid The endpoint ID. + * @param _peer The address of the peer to be associated with the corresponding endpoint. + */ + function setPeer(uint32 _eid, bytes32 _peer) external; + + /** + * @notice Sets the delegate address for the OApp Core. + * @param _delegate The address of the delegate to be set. + */ + function setDelegate(address _delegate) external; +} diff --git a/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppMsgInspector.sol b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppMsgInspector.sol new file mode 100644 index 000000000..22c0ef964 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppMsgInspector.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @title IOAppMsgInspector + * @dev Interface for the OApp Message Inspector, allowing examination of message and options contents. + */ +interface IOAppMsgInspector { + // Custom error message for inspection failure + error InspectionFailed(bytes message, bytes options); + + /** + * @notice Allows the inspector to examine LayerZero message contents and optionally throw a revert if invalid. + * @param _message The message payload to be inspected. + * @param _options Additional options or parameters for inspection. + * @return valid A boolean indicating whether the inspection passed (true) or failed (false). + * + * @dev Optionally done as a revert, OR use the boolean provided to handle the failure. + */ + function inspect(bytes calldata _message, bytes calldata _options) external view returns (bool valid); +} diff --git a/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppOptionsType3.sol b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppOptionsType3.sol new file mode 100644 index 000000000..e0139733f --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppOptionsType3.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Struct representing enforced option parameters. + */ +struct EnforcedOptionParam { + uint32 eid; // Endpoint ID + uint16 msgType; // Message Type + bytes options; // Additional options +} + +/** + * @title IOAppOptionsType3 + * @dev Interface for the OApp with Type 3 Options, allowing the setting and combining of enforced options. + */ +interface IOAppOptionsType3 { + // Custom error message for invalid options + error InvalidOptions(bytes options); + + // Event emitted when enforced options are set + event EnforcedOptionSet(EnforcedOptionParam[] _enforcedOptions); + + /** + * @notice Sets enforced options for specific endpoint and message type combinations. + * @param _enforcedOptions An array of EnforcedOptionParam structures specifying enforced options. + */ + function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) external; + + /** + * @notice Combines options for a given endpoint and message type. + * @param _eid The endpoint ID. + * @param _msgType The OApp message type. + * @param _extraOptions Additional options passed by the caller. + * @return options The combination of caller specified options AND enforced options. + */ + function combineOptions( + uint32 _eid, + uint16 _msgType, + bytes calldata _extraOptions + ) external view returns (bytes memory options); +} diff --git a/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppReceiver.sol b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppReceiver.sol new file mode 100644 index 000000000..425f9f5eb --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/interfaces/IOAppReceiver.sol @@ -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); +} diff --git a/contracts/GHST/GHSTOFT/oapp/libs/OAppOptionsType3.sol b/contracts/GHST/GHSTOFT/oapp/libs/OAppOptionsType3.sol new file mode 100644 index 000000000..5b1159a36 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/libs/OAppOptionsType3.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IOAppOptionsType3, EnforcedOptionParam } from "../interfaces/IOAppOptionsType3.sol"; + +/** + * @title OAppOptionsType3 + * @dev Abstract contract implementing the IOAppOptionsType3 interface with type 3 options. + */ +abstract contract OAppOptionsType3 is IOAppOptionsType3, Ownable { + uint16 internal constant OPTION_TYPE_3 = 3; + + // @dev The "msgType" should be defined in the child contract. + mapping(uint32 eid => mapping(uint16 msgType => bytes enforcedOption)) public enforcedOptions; + + /** + * @dev Sets the enforced options for specific endpoint and message type combinations. + * @param _enforcedOptions An array of EnforcedOptionParam structures specifying enforced options. + * + * @dev Only the owner/admin of the OApp can call this function. + * @dev Provides a way for the OApp to enforce things like paying for PreCrime, AND/OR minimum dst lzReceive gas amounts etc. + * @dev These enforced options can vary as the potential options/execution on the remote may differ as per the msgType. + * eg. Amount of lzReceive() gas necessary to deliver a lzCompose() message adds overhead you dont want to pay + * if you are only making a standard LayerZero message ie. lzReceive() WITHOUT sendCompose(). + */ + function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) public virtual onlyOwner { + for (uint256 i = 0; i < _enforcedOptions.length; i++) { + // @dev Enforced options are only available for optionType 3, as type 1 and 2 dont support combining. + _assertOptionsType3(_enforcedOptions[i].options); + enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options; + } + + emit EnforcedOptionSet(_enforcedOptions); + } + + /** + * @notice Combines options for a given endpoint and message type. + * @param _eid The endpoint ID. + * @param _msgType The OAPP message type. + * @param _extraOptions Additional options passed by the caller. + * @return options The combination of caller specified options AND enforced options. + * + * @dev If there is an enforced lzReceive option: + * - {gasLimit: 200k, msg.value: 1 ether} AND a caller supplies a lzReceive option: {gasLimit: 100k, msg.value: 0.5 ether} + * - The resulting options will be {gasLimit: 300k, msg.value: 1.5 ether} when the message is executed on the remote lzReceive() function. + * @dev This presence of duplicated options is handled off-chain in the verifier/executor. + */ + function combineOptions( + uint32 _eid, + uint16 _msgType, + bytes calldata _extraOptions + ) public view virtual returns (bytes memory) { + bytes memory enforced = enforcedOptions[_eid][_msgType]; + + // No enforced options, pass whatever the caller supplied, even if it's empty or legacy type 1/2 options. + if (enforced.length == 0) return _extraOptions; + + // No caller options, return enforced + if (_extraOptions.length == 0) return enforced; + + // @dev If caller provided _extraOptions, must be type 3 as its the ONLY type that can be combined. + if (_extraOptions.length >= 2) { + _assertOptionsType3(_extraOptions); + // @dev Remove the first 2 bytes containing the type from the _extraOptions and combine with enforced. + return bytes.concat(enforced, _extraOptions[2:]); + } + + // No valid set of options was found. + revert InvalidOptions(_extraOptions); + } + + /** + * @dev Internal function to assert that options are of type 3. + * @param _options The options to be checked. + */ + function _assertOptionsType3(bytes calldata _options) internal pure virtual { + uint16 optionsType = uint16(bytes2(_options[0:2])); + if (optionsType != OPTION_TYPE_3) revert InvalidOptions(_options); + } +} diff --git a/contracts/GHST/GHSTOFT/oapp/libs/OptionsBuilder.sol b/contracts/GHST/GHSTOFT/oapp/libs/OptionsBuilder.sol new file mode 100644 index 000000000..b6748bd75 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oapp/libs/OptionsBuilder.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { BytesLib } from "solidity-bytes-utils/contracts/BytesLib.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import { DVNOptions } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/DVNOptions.sol"; + +/** + * @title OptionsBuilder + * @dev Library for building and encoding various message options. + */ +library OptionsBuilder { + using SafeCast for uint256; + using BytesLib for bytes; + + // Constants for options types + uint16 internal constant TYPE_1 = 1; // legacy options type 1 + uint16 internal constant TYPE_2 = 2; // legacy options type 2 + uint16 internal constant TYPE_3 = 3; + + // Custom error message + error InvalidSize(uint256 max, uint256 actual); + error InvalidOptionType(uint16 optionType); + + // Modifier to ensure only options of type 3 are used + modifier onlyType3(bytes memory _options) { + if (_options.toUint16(0) != TYPE_3) revert InvalidOptionType(_options.toUint16(0)); + _; + } + + /** + * @dev Creates a new options container with type 3. + * @return options The newly created options container. + */ + function newOptions() internal pure returns (bytes memory) { + return abi.encodePacked(TYPE_3); + } + + /** + * @dev Adds an executor LZ receive option to the existing options. + * @param _options The existing options container. + * @param _gas The gasLimit used on the lzReceive() function in the OApp. + * @param _value The msg.value passed to the lzReceive() function in the OApp. + * @return options The updated options container. + * + * @dev When multiples of this option are added, they are summed by the executor + * eg. if (_gas: 200k, and _value: 1 ether) AND (_gas: 100k, _value: 0.5 ether) are sent in an option to the LayerZeroEndpoint, + * that becomes (300k, 1.5 ether) when the message is executed on the remote lzReceive() function. + */ + function addExecutorLzReceiveOption( + bytes memory _options, + uint128 _gas, + uint128 _value + ) internal pure onlyType3(_options) returns (bytes memory) { + bytes memory option = ExecutorOptions.encodeLzReceiveOption(_gas, _value); + return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_LZRECEIVE, option); + } + + /** + * @dev Adds an executor native drop option to the existing options. + * @param _options The existing options container. + * @param _amount The amount for the native value that is airdropped to the 'receiver'. + * @param _receiver The receiver address for the native drop option. + * @return options The updated options container. + * + * @dev When multiples of this option are added, they are summed by the executor on the remote chain. + */ + function addExecutorNativeDropOption( + bytes memory _options, + uint128 _amount, + bytes32 _receiver + ) internal pure onlyType3(_options) returns (bytes memory) { + bytes memory option = ExecutorOptions.encodeNativeDropOption(_amount, _receiver); + return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_NATIVE_DROP, option); + } + + /** + * @dev Adds an executor LZ compose option to the existing options. + * @param _options The existing options container. + * @param _index The index for the lzCompose() function call. + * @param _gas The gasLimit for the lzCompose() function call. + * @param _value The msg.value for the lzCompose() function call. + * @return options The updated options container. + * + * @dev When multiples of this option are added, they are summed PER index by the executor on the remote chain. + * @dev If the OApp sends N lzCompose calls on the remote, you must provide N incremented indexes starting with 0. + * ie. When your remote OApp composes (N = 3) messages, you must set this option for index 0,1,2 + */ + function addExecutorLzComposeOption( + bytes memory _options, + uint16 _index, + uint128 _gas, + uint128 _value + ) internal pure onlyType3(_options) returns (bytes memory) { + bytes memory option = ExecutorOptions.encodeLzComposeOption(_index, _gas, _value); + return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_LZCOMPOSE, option); + } + + /** + * @dev Adds an executor ordered execution option to the existing options. + * @param _options The existing options container. + * @return options The updated options container. + */ + function addExecutorOrderedExecutionOption( + bytes memory _options + ) internal pure onlyType3(_options) returns (bytes memory) { + return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION, bytes("")); + } + + /** + * @dev Adds a DVN pre-crime option to the existing options. + * @param _options The existing options container. + * @param _dvnIdx The DVN index for the pre-crime option. + * @return options The updated options container. + */ + function addDVNPreCrimeOption( + bytes memory _options, + uint8 _dvnIdx + ) internal pure onlyType3(_options) returns (bytes memory) { + return addDVNOption(_options, _dvnIdx, DVNOptions.OPTION_TYPE_PRECRIME, bytes("")); + } + + /** + * @dev Adds an executor option to the existing options. + * @param _options The existing options container. + * @param _optionType The type of the executor option. + * @param _option The encoded data for the executor option. + * @return options The updated options container. + */ + function addExecutorOption( + bytes memory _options, + uint8 _optionType, + bytes memory _option + ) internal pure onlyType3(_options) returns (bytes memory) { + return + abi.encodePacked( + _options, + ExecutorOptions.WORKER_ID, + _option.length.toUint16() + 1, // +1 for optionType + _optionType, + _option + ); + } + + /** + * @dev Adds a DVN option to the existing options. + * @param _options The existing options container. + * @param _dvnIdx The DVN index for the DVN option. + * @param _optionType The type of the DVN option. + * @param _option The encoded data for the DVN option. + * @return options The updated options container. + */ + function addDVNOption( + bytes memory _options, + uint8 _dvnIdx, + uint8 _optionType, + bytes memory _option + ) internal pure onlyType3(_options) returns (bytes memory) { + return + abi.encodePacked( + _options, + DVNOptions.WORKER_ID, + _option.length.toUint16() + 2, // +2 for optionType and dvnIdx + _dvnIdx, + _optionType, + _option + ); + } + + /** + * @dev Encodes legacy options of type 1. + * @param _executionGas The gasLimit value passed to lzReceive(). + * @return legacyOptions The encoded legacy options. + */ + function encodeLegacyOptionsType1(uint256 _executionGas) internal pure returns (bytes memory) { + if (_executionGas > type(uint128).max) revert InvalidSize(type(uint128).max, _executionGas); + return abi.encodePacked(TYPE_1, _executionGas); + } + + /** + * @dev Encodes legacy options of type 2. + * @param _executionGas The gasLimit value passed to lzReceive(). + * @param _nativeForDst The amount of native air dropped to the receiver. + * @param _receiver The _nativeForDst receiver address. + * @return legacyOptions The encoded legacy options of type 2. + */ + function encodeLegacyOptionsType2( + uint256 _executionGas, + uint256 _nativeForDst, + bytes memory _receiver // @dev Use bytes instead of bytes32 in legacy type 2 for _receiver. + ) internal pure returns (bytes memory) { + if (_executionGas > type(uint128).max) revert InvalidSize(type(uint128).max, _executionGas); + if (_nativeForDst > type(uint128).max) revert InvalidSize(type(uint128).max, _nativeForDst); + if (_receiver.length > 32) revert InvalidSize(32, _receiver.length); + return abi.encodePacked(TYPE_2, _executionGas, _nativeForDst, _receiver); + } +} diff --git a/contracts/GHST/GHSTOFT/oft/OFT.sol b/contracts/GHST/GHSTOFT/oft/OFT.sol new file mode 100644 index 000000000..c96364ef6 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/OFT.sol @@ -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; + } +} diff --git a/contracts/GHST/GHSTOFT/oft/OFTAdapter.sol b/contracts/GHST/GHSTOFT/oft/OFTAdapter.sol new file mode 100644 index 000000000..a3b4845d5 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/OFTAdapter.sol @@ -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; + } +} diff --git a/contracts/GHST/GHSTOFT/oft/OFTCore.sol b/contracts/GHST/GHSTOFT/oft/OFTCore.sol new file mode 100644 index 000000000..fee9e9bbd --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/OFTCore.sol @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { OApp, Origin } from "../oapp/OApp.sol"; +import { OAppOptionsType3 } from "../oapp/libs/OAppOptionsType3.sol"; +import { IOAppMsgInspector } from "../oapp/interfaces/IOAppMsgInspector.sol"; + +import { OAppPreCrimeSimulator } from "../precrime/OAppPreCrimeSimulator.sol"; + +import { IOFT, SendParam, OFTLimit, OFTReceipt, OFTFeeDetail, MessagingReceipt, MessagingFee } from "./interfaces/IOFT.sol"; +import { OFTMsgCodec } from "./libs/OFTMsgCodec.sol"; +import { OFTComposeMsgCodec } from "./libs/OFTComposeMsgCodec.sol"; + +/** + * @title OFTCore + * @dev Abstract contract for the OftChain (OFT) token. + */ +abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 { + using OFTMsgCodec for bytes; + using OFTMsgCodec for bytes32; + + // @notice Provides a conversion rate when swapping between denominations of SD and LD + // - shareDecimals == SD == shared Decimals + // - localDecimals == LD == local decimals + // @dev Considers that tokens have different decimal amounts on various chains. + // @dev eg. + // For a token + // - locally with 4 decimals --> 1.2345 => uint(12345) + // - remotely with 2 decimals --> 1.23 => uint(123) + // - The conversion rate would be 10 ** (4 - 2) = 100 + // @dev If you want to send 1.2345 -> (uint 12345), you CANNOT represent that value on the remote, + // you can only display 1.23 -> uint(123). + // @dev To preserve the dust that would otherwise be lost on that conversion, + // we need to unify a denomination that can be represented on ALL chains inside of the OFT mesh + uint256 public immutable decimalConversionRate; + + // @notice Msg types that are used to identify the various OFT operations. + // @dev This can be extended in child contracts for non-default oft operations + // @dev These values are used in things like combineOptions() in OAppOptionsType3.sol. + uint16 public constant SEND = 1; + uint16 public constant SEND_AND_CALL = 2; + + // Address of an optional contract to inspect both 'message' and 'options' + address public msgInspector; + event MsgInspectorSet(address inspector); + + /** + * @dev Constructor. + * @param _localDecimals The decimals of the token on the local chain (this chain). + * @param _endpoint The address of the LayerZero endpoint. + * @param _delegate The delegate capable of making OApp configurations inside of the endpoint. + */ + constructor(uint8 _localDecimals, address _endpoint, address _delegate) OApp(_endpoint, _delegate) { + if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); + } + + /** + * @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 pure virtual returns (uint8) { + return 6; + } + + /** + * @dev Sets the message inspector address for the OFT. + * @param _msgInspector The address of the message inspector. + * + * @dev This is an optional contract that can be used to inspect both 'message' and 'options'. + * @dev Set it to address(0) to disable it, or set it to a contract address to enable it. + */ + function setMsgInspector(address _msgInspector) public virtual onlyOwner { + msgInspector = _msgInspector; + emit MsgInspectorSet(_msgInspector); + } + + /** + * @notice Provides a quote for OFT-related operations. + * @param _sendParam The parameters for the send operation. + * @return oftLimit The OFT limit information. + * @return oftFeeDetails The details of OFT fees. + * @return oftReceipt The OFT receipt information. + */ + function quoteOFT( + SendParam calldata _sendParam + ) + external + view + virtual + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; // Unused in the default implementation. + uint256 maxAmountLD = type(uint64).max; // Unused in the default implementation. + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + // Unused in the default implementation; reserved for future complex fee details. + oftFeeDetails = new OFTFeeDetail[](0); + + // @dev This is the same as the send() operation, but without the actual send. + // - amountSentLD is the amount in local decimals that would be sent from the sender. + // - amountReceivedLD is the amount in local decimals that will be credited to the recipient on the remote OFT instance. + // @dev The amountSentLD MIGHT not equal the amount the user actually receives. HOWEVER, the default does. + (uint256 amountSentLD, uint256 amountReceivedLD) = _debitView( + _sendParam.amountLD, + _sendParam.minAmountLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); + } + + /** + * @notice Provides a quote for the send() operation. + * @param _sendParam The parameters for the send() operation. + * @param _payInLzToken Flag indicating whether the caller is paying in the LZ token. + * @return msgFee The calculated LayerZero messaging fee from the send() operation. + * + * @dev MessagingFee: LayerZero msg fee + * - nativeFee: The native fee. + * - lzTokenFee: The lzToken fee. + */ + function quoteSend( + SendParam calldata _sendParam, + bool _payInLzToken + ) external view virtual returns (MessagingFee memory msgFee) { + // @dev mock the amount to receive, this is the same operation used in the send(). + // The quote is as similar as possible to the actual send() operation. + (, 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 Calculates the LayerZero fee for the send() operation. + return _quote(_sendParam.dstEid, message, options, _payInLzToken); + } + + /** + * @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 returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) { + // @dev Applies the token transfers regarding this send() operation. + // - 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) = _debit( + _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 = _lzSend(_sendParam.dstEid, 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 build the message and options. + * @param _sendParam The parameters for the send() operation. + * @param _amountLD The amount in local decimals. + * @return message The encoded message. + * @return options The encoded options. + */ + function _buildMsgAndOptions( + SendParam calldata _sendParam, + uint256 _amountLD + ) internal view virtual returns (bytes memory message, bytes memory options) { + bool hasCompose; + // @dev This generated message has the msg.sender encoded into the payload so the remote knows who the caller is. + (message, hasCompose) = OFTMsgCodec.encode( + _sendParam.to, + _toSD(_amountLD), + // @dev Must be include a non empty bytes if you want to compose, EVEN if you dont need it on the remote. + // EVEN if you dont require an arbitrary payload to be sent... eg. '0x01' + _sendParam.composeMsg + ); + // @dev Change the msg type depending if its composed or not. + uint16 msgType = hasCompose ? SEND_AND_CALL : SEND; + // @dev Combine the callers _extraOptions with the enforced options via the OAppOptionsType3. + options = combineOptions(_sendParam.dstEid, msgType, _sendParam.extraOptions); + + // @dev Optionally inspect the message and options depending if the OApp owner has set a msg inspector. + // @dev If it fails inspection, needs to revert in the implementation. ie. does not rely on return boolean + if (msgInspector != address(0)) IOAppMsgInspector(msgInspector).inspect(message, options); + } + + /** + * @dev Internal function to handle the receive on the LayerZero endpoint. + * @param _origin The origin information. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address from the src chain. + * - nonce: The nonce of the LayerZero message. + * @param _guid The unique identifier for the received LayerZero message. + * @param _message The encoded message. + * @dev _executor The address of the executor. + * @dev _extraData Additional data. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address /*_executor*/, // @dev unused in the default implementation. + bytes calldata /*_extraData*/ // @dev unused in the default implementation. + ) internal virtual override { + // @dev The src sending chain doesnt know the address length on this chain (potentially non-evm) + // Thus everything is bytes32() encoded in flight. + address toAddress = _message.sendTo().bytes32ToAddress(); + // @dev Credit the amountLD to the recipient and return the ACTUAL amount the recipient received in local decimals + uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid); + + if (_message.isComposed()) { + // @dev Proprietary composeMsg format for the OFT. + bytes memory composeMsg = OFTComposeMsgCodec.encode( + _origin.nonce, + _origin.srcEid, + amountReceivedLD, + _message.composeMsg() + ); + + // @dev Stores the lzCompose payload that will be executed in a separate tx. + // Standardizes functionality for executing arbitrary contract invocation on some non-evm chains. + // @dev The off-chain executor will listen and process the msg based on the src-chain-callers compose options passed. + // @dev The index is used when a OApp needs to compose multiple msgs on lzReceive. + // For default OFT implementation there is only 1 compose msg per lzReceive, thus its always 0. + endpoint.sendCompose(toAddress, _guid, 0 /* the index of the composed message*/, composeMsg); + } + + emit OFTReceived(_guid, _origin.srcEid, toAddress, amountReceivedLD); + } + + /** + * @dev Internal function to handle the OAppPreCrimeSimulator simulated receive. + * @param _origin The origin information. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address from the src chain. + * - nonce: The nonce of the LayerZero message. + * @param _guid The unique identifier for the received LayerZero message. + * @param _message The LayerZero message. + * @param _executor The address of the off-chain executor. + * @param _extraData Arbitrary data passed by the msg executor. + * + * @dev Enables the preCrime simulator to mock sending lzReceive() messages, + * routes the msg down from the OAppPreCrimeSimulator, and back up to the OAppReceiver. + */ + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + /** + * @dev Check if the peer is considered 'trusted' by the OApp. + * @param _eid The endpoint ID to check. + * @param _peer The peer to check. + * @return Whether the peer passed is considered 'trusted' by the OApp. + * + * @dev Enables OAppPreCrimeSimulator to check whether a potential Inbound Packet is from a trusted source. + */ + function isPeer(uint32 _eid, bytes32 _peer) public view virtual override returns (bool) { + return peers[_eid] == _peer; + } + + /** + * @dev Internal function to remove dust from the given local decimal amount. + * @param _amountLD The amount in local decimals. + * @return amountLD The amount after removing dust. + * + * @dev Prevents the loss of dust when moving amounts between chains with different decimals. + * @dev eg. uint(123) with a conversion rate of 100 becomes uint(100). + */ + function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) { + return (_amountLD / decimalConversionRate) * decimalConversionRate; + } + + /** + * @dev Internal function to convert an amount from shared decimals into local decimals. + * @param _amountSD The amount in shared decimals. + * @return amountLD The amount in local decimals. + */ + function _toLD(uint64 _amountSD) internal view virtual returns (uint256 amountLD) { + return _amountSD * decimalConversionRate; + } + + /** + * @dev Internal function to convert an amount from local decimals into shared decimals. + * @param _amountLD The amount in local decimals. + * @return amountSD The amount in shared decimals. + */ + function _toSD(uint256 _amountLD) internal view virtual returns (uint64 amountSD) { + return uint64(_amountLD / decimalConversionRate); + } + + /** + * @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 returns (uint256 amountSentLD, uint256 amountReceivedLD) { + // @dev Remove the dust so nothing is lost on the conversion between chains with different decimals for the token. + amountSentLD = _removeDust(_amountLD); + // @dev The amount to send is the same as amount received in the default implementation. + amountReceivedLD = amountSentLD; + + // @dev Check for slippage. + if (amountReceivedLD < _minAmountLD) { + revert SlippageExceeded(amountReceivedLD, _minAmountLD); + } + } + + /** + * @dev Internal function to perform a debit operation. + * @param _amountLD The amount to send in local decimals. + * @param _minAmountLD The minimum amount to send in local decimals. + * @param _dstEid The destination endpoint ID. + * @return amountSentLD The amount sent in local decimals. + * @return amountReceivedLD The amount received in local decimals on the remote. + * + * @dev Defined here but are intended to be overriden depending on the OFT implementation. + * @dev Depending on OFT implementation the _amountLD could differ from the amountReceivedLD. + */ + function _debit( + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) internal virtual returns (uint256 amountSentLD, uint256 amountReceivedLD); + + /** + * @dev Internal function to perform a credit operation. + * @param _to The address to credit. + * @param _amountLD The amount to credit in local decimals. + * @param _srcEid The source endpoint ID. + * @return amountReceivedLD The amount ACTUALLY received in local decimals. + * + * @dev Defined here but are intended to be overriden depending on the OFT implementation. + * @dev Depending on OFT implementation the _amountLD could differ from the amountReceivedLD. + */ + function _credit( + address _to, + uint256 _amountLD, + uint32 _srcEid + ) internal virtual returns (uint256 amountReceivedLD); +} diff --git a/contracts/GHST/GHSTOFT/oft/interfaces/IOFT.sol b/contracts/GHST/GHSTOFT/oft/interfaces/IOFT.sol new file mode 100644 index 000000000..e07541970 --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/interfaces/IOFT.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { MessagingReceipt, MessagingFee } from "../../oapp/OAppSender.sol"; + +/** + * @dev Struct representing token parameters for the OFT send() operation. + */ +struct SendParam { + uint32 dstEid; // Destination endpoint ID. + bytes32 to; // Recipient address. + uint256 amountLD; // Amount to send in local decimals. + uint256 minAmountLD; // Minimum amount to send in local decimals. + bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message. + bytes composeMsg; // The composed message for the send() operation. + bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations. +} + +/** + * @dev Struct representing OFT limit information. + * @dev These amounts can change dynamically and are up the the specific oft implementation. + */ +struct OFTLimit { + uint256 minAmountLD; // Minimum amount in local decimals that can be sent to the recipient. + uint256 maxAmountLD; // Maximum amount in local decimals that can be sent to the recipient. +} + +/** + * @dev Struct representing OFT receipt information. + */ +struct OFTReceipt { + uint256 amountSentLD; // Amount of tokens ACTUALLY debited from the sender in local decimals. + // @dev In non-default implementations, the amountReceivedLD COULD differ from this value. + uint256 amountReceivedLD; // Amount of tokens to be received on the remote side. +} + +/** + * @dev Struct representing OFT fee details. + * @dev Future proof mechanism to provide a standardized way to communicate fees to things like a UI. + */ +struct OFTFeeDetail { + int256 feeAmountLD; // Amount of the fee in local decimals. + string description; // Description of the fee. +} + +/** + * @title IOFT + * @dev Interface for the OftChain (OFT) token. + * @dev Does not inherit ERC20 to accommodate usage by OFTAdapter as well. + * @dev This specific interface ID is '0x02e49c2c'. + */ +interface IOFT { + // Custom error messages + error InvalidLocalDecimals(); + error SlippageExceeded(uint256 amountLD, uint256 minAmountLD); + + // Events + event OFTSent( + bytes32 indexed guid, // GUID of the OFT message. + uint32 dstEid, // Destination Endpoint ID. + address indexed fromAddress, // Address of the sender on the src chain. + uint256 amountSentLD, // Amount of tokens sent in local decimals. + uint256 amountReceivedLD // Amount of tokens received in local decimals. + ); + event OFTReceived( + bytes32 indexed guid, // GUID of the OFT message. + uint32 srcEid, // Source Endpoint ID. + address indexed toAddress, // Address of the recipient on the dst chain. + uint256 amountReceivedLD // Amount of tokens received in local decimals. + ); + + /** + * @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 view returns (bytes4 interfaceId, uint64 version); + + /** + * @notice Retrieves the address of the token associated with the OFT. + * @return token The address of the ERC20 token implementation. + */ + function token() external view returns (address); + + /** + * @notice Indicates whether the OFT contract requires approval of the 'token()' to send. + * @return requiresApproval Needs approval of the underlying token implementation. + * + * @dev Allows things like wallet implementers to determine integration requirements, + * without understanding the underlying token implementation. + */ + function approvalRequired() external view returns (bool); + + /** + * @notice Retrieves the shared decimals of the OFT. + * @return sharedDecimals The shared decimals of the OFT. + */ + function sharedDecimals() external view returns (uint8); + + /** + * @notice Provides a quote for OFT-related operations. + * @param _sendParam The parameters for the send operation. + * @return limit The OFT limit information. + * @return oftFeeDetails The details of OFT fees. + * @return receipt The OFT receipt information. + */ + function quoteOFT( + SendParam calldata _sendParam + ) external view returns (OFTLimit memory, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory); + + /** + * @notice Provides a quote for the send() operation. + * @param _sendParam The parameters for the send() operation. + * @param _payInLzToken Flag indicating whether the caller is paying in the LZ token. + * @return fee The calculated LayerZero messaging fee from the send() operation. + * + * @dev MessagingFee: LayerZero msg fee + * - nativeFee: The native fee. + * - lzTokenFee: The lzToken fee. + */ + function quoteSend(SendParam calldata _sendParam, bool _payInLzToken) external view returns (MessagingFee memory); + + /** + * @notice Executes the send() operation. + * @param _sendParam The parameters for the send operation. + * @param _fee The fee information supplied by the caller. + * - nativeFee: The native fee. + * - lzTokenFee: The lzToken fee. + * @param _refundAddress The address to receive any excess funds from fees etc. on the src. + * @return receipt The LayerZero messaging receipt from 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 returns (MessagingReceipt memory, OFTReceipt memory); +} diff --git a/contracts/GHST/GHSTOFT/oft/libs/OFTComposeMsgCodec.sol b/contracts/GHST/GHSTOFT/oft/libs/OFTComposeMsgCodec.sol new file mode 100644 index 000000000..13e3ffb0c --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/libs/OFTComposeMsgCodec.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library OFTComposeMsgCodec { + // Offset constants for decoding composed messages + uint8 private constant NONCE_OFFSET = 8; + uint8 private constant SRC_EID_OFFSET = 12; + uint8 private constant AMOUNT_LD_OFFSET = 44; + uint8 private constant COMPOSE_FROM_OFFSET = 76; + + /** + * @dev Encodes a OFT composed message. + * @param _nonce The nonce value. + * @param _srcEid The source endpoint ID. + * @param _amountLD The amount in local decimals. + * @param _composeMsg The composed message. + * @return _msg The encoded Composed message. + */ + function encode( + uint64 _nonce, + uint32 _srcEid, + uint256 _amountLD, + bytes memory _composeMsg // 0x[composeFrom][composeMsg] + ) internal pure returns (bytes memory _msg) { + _msg = abi.encodePacked(_nonce, _srcEid, _amountLD, _composeMsg); + } + + /** + * @dev Retrieves the nonce from the composed message. + * @param _msg The message. + * @return The nonce value. + */ + function nonce(bytes calldata _msg) internal pure returns (uint64) { + return uint64(bytes8(_msg[:NONCE_OFFSET])); + } + + /** + * @dev Retrieves the source endpoint ID from the composed message. + * @param _msg The message. + * @return The source endpoint ID. + */ + function srcEid(bytes calldata _msg) internal pure returns (uint32) { + return uint32(bytes4(_msg[NONCE_OFFSET:SRC_EID_OFFSET])); + } + + /** + * @dev Retrieves the amount in local decimals from the composed message. + * @param _msg The message. + * @return The amount in local decimals. + */ + function amountLD(bytes calldata _msg) internal pure returns (uint256) { + return uint256(bytes32(_msg[SRC_EID_OFFSET:AMOUNT_LD_OFFSET])); + } + + /** + * @dev Retrieves the composeFrom value from the composed message. + * @param _msg The message. + * @return The composeFrom value. + */ + function composeFrom(bytes calldata _msg) internal pure returns (bytes32) { + return bytes32(_msg[AMOUNT_LD_OFFSET:COMPOSE_FROM_OFFSET]); + } + + /** + * @dev Retrieves the composed message. + * @param _msg The message. + * @return The composed message. + */ + function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { + return _msg[COMPOSE_FROM_OFFSET:]; + } + + /** + * @dev Converts an address to bytes32. + * @param _addr The address to convert. + * @return The bytes32 representation of the address. + */ + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + /** + * @dev Converts bytes32 to an address. + * @param _b The bytes32 value to convert. + * @return The address representation of bytes32. + */ + function bytes32ToAddress(bytes32 _b) internal pure returns (address) { + return address(uint160(uint256(_b))); + } +} diff --git a/contracts/GHST/GHSTOFT/oft/libs/OFTMsgCodec.sol b/contracts/GHST/GHSTOFT/oft/libs/OFTMsgCodec.sol new file mode 100644 index 000000000..f57a82e6c --- /dev/null +++ b/contracts/GHST/GHSTOFT/oft/libs/OFTMsgCodec.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library OFTMsgCodec { + // Offset constants for encoding and decoding OFT messages + uint8 private constant SEND_TO_OFFSET = 32; + uint8 private constant SEND_AMOUNT_SD_OFFSET = 40; + + /** + * @dev Encodes an OFT LayerZero message. + * @param _sendTo The recipient address. + * @param _amountShared The amount in shared decimals. + * @param _composeMsg The composed message. + * @return _msg The encoded message. + * @return hasCompose A boolean indicating whether the message has a composed payload. + */ + function encode( + bytes32 _sendTo, + uint64 _amountShared, + bytes memory _composeMsg + ) internal view returns (bytes memory _msg, bool hasCompose) { + hasCompose = _composeMsg.length > 0; + // @dev Remote chains will want to know the composed function caller ie. msg.sender on the src. + _msg = hasCompose + ? abi.encodePacked(_sendTo, _amountShared, addressToBytes32(msg.sender), _composeMsg) + : abi.encodePacked(_sendTo, _amountShared); + } + + /** + * @dev Checks if the OFT message is composed. + * @param _msg The OFT message. + * @return A boolean indicating whether the message is composed. + */ + function isComposed(bytes calldata _msg) internal pure returns (bool) { + return _msg.length > SEND_AMOUNT_SD_OFFSET; + } + + /** + * @dev Retrieves the recipient address from the OFT message. + * @param _msg The OFT message. + * @return The recipient address. + */ + function sendTo(bytes calldata _msg) internal pure returns (bytes32) { + return bytes32(_msg[:SEND_TO_OFFSET]); + } + + /** + * @dev Retrieves the amount in shared decimals from the OFT message. + * @param _msg The OFT message. + * @return The amount in shared decimals. + */ + function amountSD(bytes calldata _msg) internal pure returns (uint64) { + return uint64(bytes8(_msg[SEND_TO_OFFSET:SEND_AMOUNT_SD_OFFSET])); + } + + /** + * @dev Retrieves the composed message from the OFT message. + * @param _msg The OFT message. + * @return The composed message. + */ + function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { + return _msg[SEND_AMOUNT_SD_OFFSET:]; + } + + /** + * @dev Converts an address to bytes32. + * @param _addr The address to convert. + * @return The bytes32 representation of the address. + */ + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + /** + * @dev Converts bytes32 to an address. + * @param _b The bytes32 value to convert. + * @return The address representation of bytes32. + */ + function bytes32ToAddress(bytes32 _b) internal pure returns (address) { + return address(uint160(uint256(_b))); + } +} diff --git a/contracts/GHST/GHSTOFT/precrime/OAppPreCrimeSimulator.sol b/contracts/GHST/GHSTOFT/precrime/OAppPreCrimeSimulator.sol new file mode 100644 index 000000000..0355279b2 --- /dev/null +++ b/contracts/GHST/GHSTOFT/precrime/OAppPreCrimeSimulator.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IPreCrime } from "./interfaces/IPreCrime.sol"; +import { IOAppPreCrimeSimulator, InboundPacket, Origin } from "./interfaces/IOAppPreCrimeSimulator.sol"; + +/** + * @title OAppPreCrimeSimulator + * @dev Abstract contract serving as the base for preCrime simulation functionality in an OApp. + */ +abstract contract OAppPreCrimeSimulator is IOAppPreCrimeSimulator, Ownable { + // The address of the preCrime implementation. + address public preCrime; + + /** + * @dev Retrieves the address of the OApp contract. + * @return The address of the OApp contract. + * + * @dev The simulator contract is the base contract for the OApp by default. + * @dev If the simulator is a separate contract, override this function. + */ + function oApp() external view virtual returns (address) { + return address(this); + } + + /** + * @dev Sets the preCrime contract address. + * @param _preCrime The address of the preCrime contract. + */ + function setPreCrime(address _preCrime) public virtual onlyOwner { + preCrime = _preCrime; + emit PreCrimeSet(_preCrime); + } + + /** + * @dev Interface for pre-crime simulations. Always reverts at the end with the simulation results. + * @param _packets An array of InboundPacket objects representing received packets to be delivered. + * + * @dev WARNING: MUST revert at the end with the simulation results. + * @dev Gives the preCrime implementation the ability to mock sending packets to the lzReceive function, + * WITHOUT actually executing them. + */ + function lzReceiveAndRevert(InboundPacket[] calldata _packets) public payable virtual { + for (uint256 i = 0; i < _packets.length; i++) { + InboundPacket calldata packet = _packets[i]; + + // Ignore packets that are not from trusted peers. + if (!isPeer(packet.origin.srcEid, packet.origin.sender)) continue; + + // @dev Because a verifier is calling this function, it doesnt have access to executor params: + // - address _executor + // - bytes calldata _extraData + // preCrime will NOT work for OApps that rely on these two parameters inside of their _lzReceive(). + // They are instead stubbed to default values, address(0) and bytes("") + // @dev Calling this.lzReceiveSimulate removes ability for assembly return 0 callstack exit, + // which would cause the revert to be ignored. + this.lzReceiveSimulate{ value: packet.value }( + packet.origin, + packet.guid, + packet.message, + packet.executor, + packet.extraData + ); + } + + // @dev Revert with the simulation results. msg.sender must implement IPreCrime.buildSimulationResult(). + revert SimulationResult(IPreCrime(msg.sender).buildSimulationResult()); + } + + /** + * @dev Is effectively an internal function because msg.sender must be address(this). + * Allows resetting the call stack for 'internal' calls. + * @param _origin The origin information containing the source endpoint and sender address. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address on the src chain. + * - nonce: The nonce of the message. + * @param _guid The unique identifier of the packet. + * @param _message The message payload of the packet. + * @param _executor The executor address for the packet. + * @param _extraData Additional data for the packet. + */ + function lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable virtual { + // @dev Ensure ONLY can be called 'internally'. + if (msg.sender != address(this)) revert OnlySelf(); + _lzReceiveSimulate(_origin, _guid, _message, _executor, _extraData); + } + + /** + * @dev Internal function to handle the OAppPreCrimeSimulator simulated receive. + * @param _origin The origin information. + * - srcEid: The source chain endpoint ID. + * - sender: The sender address from the src chain. + * - nonce: The nonce of the LayerZero message. + * @param _guid The GUID of the LayerZero message. + * @param _message The LayerZero message. + * @param _executor The address of the off-chain executor. + * @param _extraData Arbitrary data passed by the msg executor. + * + * @dev Enables the preCrime simulator to mock sending lzReceive() messages, + * routes the msg down from the OAppPreCrimeSimulator, and back up to the OAppReceiver. + */ + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; + + /** + * @dev checks if the specified peer is considered 'trusted' by the OApp. + * @param _eid The endpoint Id to check. + * @param _peer The peer to check. + * @return Whether the peer passed is considered 'trusted' by the OApp. + */ + function isPeer(uint32 _eid, bytes32 _peer) public view virtual returns (bool); +} diff --git a/contracts/GHST/GHSTOFT/precrime/PreCrime.sol b/contracts/GHST/GHSTOFT/precrime/PreCrime.sol new file mode 100644 index 000000000..f1e2b4623 --- /dev/null +++ b/contracts/GHST/GHSTOFT/precrime/PreCrime.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { BytesLib } from "solidity-bytes-utils/contracts/BytesLib.sol"; +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { IPreCrime, PreCrimePeer } from "./interfaces/IPreCrime.sol"; +import { IOAppPreCrimeSimulator } from "./interfaces/IOAppPreCrimeSimulator.sol"; +import { InboundPacket, PacketDecoder } from "./libs/Packet.sol"; + +abstract contract PreCrime is Ownable, IPreCrime { + using BytesLib for bytes; + + uint16 internal constant CONFIG_VERSION = 2; + address internal constant OFF_CHAIN_CALLER = address(0xDEAD); + + address internal immutable lzEndpoint; + address public immutable simulator; + address public immutable oApp; + + // preCrime config + uint64 public maxBatchSize; + PreCrimePeer[] internal preCrimePeers; + + /// @dev getConfig(), simulate() and preCrime() are not view functions because it is more flexible to be able to + /// update state for some complex logic. So onlyOffChain() modifier is to make sure they are only called + /// by the off-chain. + modifier onlyOffChain() { + if (msg.sender != OFF_CHAIN_CALLER) revert OnlyOffChain(); + _; + } + + constructor(address _endpoint, address _simulator) { + lzEndpoint = _endpoint; + simulator = _simulator; + oApp = IOAppPreCrimeSimulator(_simulator).oApp(); + } + + function setMaxBatchSize(uint64 _maxBatchSize) external onlyOwner { + maxBatchSize = _maxBatchSize; + } + + function setPreCrimePeers(PreCrimePeer[] calldata _preCrimePeers) external onlyOwner { + delete preCrimePeers; + for (uint256 i = 0; i < _preCrimePeers.length; ++i) { + preCrimePeers.push(_preCrimePeers[i]); + } + } + + function getPreCrimePeers() external view returns (PreCrimePeer[] memory) { + return preCrimePeers; + } + + function getConfig( + bytes[] calldata _packets, + uint256[] calldata _packetMsgValues + ) external onlyOffChain returns (bytes memory) { + bytes memory config = abi.encodePacked(CONFIG_VERSION, maxBatchSize); + + // if no packets, return config with all peers + PreCrimePeer[] memory peers = _packets.length == 0 + ? preCrimePeers + : _getPreCrimePeers(PacketDecoder.decode(_packets, _packetMsgValues)); + + if (peers.length > 0) { + uint16 size = uint16(peers.length); + config = abi.encodePacked(config, size); + + for (uint256 i = 0; i < size; ++i) { + config = abi.encodePacked(config, peers[i].eid, peers[i].preCrime, peers[i].oApp); + } + } + + return config; + } + + // @dev _packetMsgValues refers to the 'lzReceive' option passed per packet + function simulate( + bytes[] calldata _packets, + uint256[] calldata _packetMsgValues + ) external payable override onlyOffChain returns (bytes memory) { + InboundPacket[] memory packets = PacketDecoder.decode(_packets, _packetMsgValues); + _checkPacketSizeAndOrder(packets); + return _simulate(packets); + } + + function preCrime( + bytes[] calldata _packets, + uint256[] calldata _packetMsgValues, + bytes[] calldata _simulations + ) external onlyOffChain { + InboundPacket[] memory packets = PacketDecoder.decode(_packets, _packetMsgValues); + uint32[] memory eids = new uint32[](_simulations.length); + bytes[] memory simulations = new bytes[](_simulations.length); + + for (uint256 i = 0; i < _simulations.length; ++i) { + bytes calldata simulation = _simulations[i]; + eids[i] = uint32(bytes4(simulation[0:4])); + simulations[i] = simulation[4:]; + } + _checkResultsCompleteness(packets, eids); + + _preCrime(packets, eids, simulations); + } + + function version() external pure returns (uint64 major, uint8 minor) { + return (2, 0); + } + + function _checkResultsCompleteness(InboundPacket[] memory _packets, uint32[] memory _eids) internal { + // check if all peers result included + if (_packets.length > 0) { + PreCrimePeer[] memory peers = _getPreCrimePeers(_packets); + for (uint256 i = 0; i < peers.length; i++) { + uint32 expectedEid = peers[i].eid; + if (!_isContain(_eids, expectedEid)) revert SimulationResultNotFound(expectedEid); + } + } + + // check if local result included + uint32 localEid = _getLocalEid(); + if (!_isContain(_eids, localEid)) revert SimulationResultNotFound(localEid); + } + + function _isContain(uint32[] memory _array, uint32 _item) internal pure returns (bool) { + for (uint256 i = 0; i < _array.length; i++) { + if (_array[i] == _item) return true; + } + return false; + } + + function _checkPacketSizeAndOrder(InboundPacket[] memory _packets) internal view { + if (_packets.length > maxBatchSize) revert PacketOversize(maxBatchSize, _packets.length); + + // check packets nonce, sequence order + // packets should group by srcEid and sender, then sort by nonce ascending + if (_packets.length > 0) { + uint32 srcEid; + bytes32 sender; + uint64 nonce; + for (uint256 i = 0; i < _packets.length; i++) { + InboundPacket memory packet = _packets[i]; + + // skip if not from trusted peer + if (!IOAppPreCrimeSimulator(simulator).isPeer(packet.origin.srcEid, packet.origin.sender)) continue; + + // start from a new chain or a new source oApp + if (packet.origin.srcEid != srcEid || packet.origin.sender != sender) { + srcEid = packet.origin.srcEid; + sender = packet.origin.sender; + nonce = _getInboundNonce(srcEid, sender); + } + // TODO ?? + // Wont the nonce order not matter and enforced at the OApp level? the simulation will revert? + + // the following packet's nonce add 1 in order + if (packet.origin.nonce != ++nonce) revert PacketUnsorted(); + } + } + } + + function _simulate(InboundPacket[] memory _packets) internal virtual returns (bytes memory) { + (bool success, bytes memory returnData) = simulator.call{ value: msg.value }( + abi.encodeWithSelector(IOAppPreCrimeSimulator.lzReceiveAndRevert.selector, _packets) + ); + + bytes memory result = _parseRevertResult(success, returnData); + return abi.encodePacked(_getLocalEid(), result); // add localEid at the first of the result + } + + function _parseRevertResult(bool _success, bytes memory _returnData) internal pure returns (bytes memory result) { + // should always revert with LzReceiveRevert + if (_success) revert SimulationFailed("no revert"); + + // if not expected selector, bubble up error + if (bytes4(_returnData) != IOAppPreCrimeSimulator.SimulationResult.selector) { + revert SimulationFailed(_returnData); + } + + // Slice the sighash. Remove the selector which is the first 4 bytes + result = _returnData.slice(4, _returnData.length - 4); + result = abi.decode(result, (bytes)); + } + + // to be compatible with EndpointV1 + function _getLocalEid() internal view virtual returns (uint32) { + return ILayerZeroEndpointV2(lzEndpoint).eid(); + } + + // to be compatible with EndpointV1 + function _getInboundNonce(uint32 _srcEid, bytes32 _sender) internal view virtual returns (uint64) { + return ILayerZeroEndpointV2(lzEndpoint).inboundNonce(oApp, _srcEid, _sender); + } + + // ----------------- to be implemented ----------------- + function buildSimulationResult() external view virtual override returns (bytes memory); + + function _getPreCrimePeers(InboundPacket[] memory _packets) internal virtual returns (PreCrimePeer[] memory peers); + + function _preCrime( + InboundPacket[] memory _packets, + uint32[] memory _eids, + bytes[] memory _simulations + ) internal virtual; +} diff --git a/contracts/GHST/GHSTOFT/precrime/interfaces/IOAppPreCrimeSimulator.sol b/contracts/GHST/GHSTOFT/precrime/interfaces/IOAppPreCrimeSimulator.sol new file mode 100644 index 000000000..96168ea2b --- /dev/null +++ b/contracts/GHST/GHSTOFT/precrime/interfaces/IOAppPreCrimeSimulator.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +// @dev Import the Origin so it's exposed to OAppPreCrimeSimulator implementers. +// solhint-disable-next-line no-unused-import +import { InboundPacket, Origin } from "../libs/Packet.sol"; + +/** + * @title IOAppPreCrimeSimulator Interface + * @dev Interface for the preCrime simulation functionality in an OApp. + */ +interface IOAppPreCrimeSimulator { + // @dev simulation result used in PreCrime implementation + error SimulationResult(bytes result); + error OnlySelf(); + + /** + * @dev Emitted when the preCrime contract address is set. + * @param preCrimeAddress The address of the preCrime contract. + */ + event PreCrimeSet(address preCrimeAddress); + + /** + * @dev Retrieves the address of the preCrime contract implementation. + * @return The address of the preCrime contract. + */ + function preCrime() external view returns (address); + + /** + * @dev Retrieves the address of the OApp contract. + * @return The address of the OApp contract. + */ + function oApp() external view returns (address); + + /** + * @dev Sets the preCrime contract address. + * @param _preCrime The address of the preCrime contract. + */ + function setPreCrime(address _preCrime) external; + + /** + * @dev Mocks receiving a packet, then reverts with a series of data to infer the state/result. + * @param _packets An array of LayerZero InboundPacket objects representing received packets. + */ + function lzReceiveAndRevert(InboundPacket[] calldata _packets) external payable; + + /** + * @dev checks if the specified peer is considered 'trusted' by the OApp. + * @param _eid The endpoint Id to check. + * @param _peer The peer to check. + * @return Whether the peer passed is considered 'trusted' by the OApp. + */ + function isPeer(uint32 _eid, bytes32 _peer) external view returns (bool); +} diff --git a/contracts/GHST/GHSTOFT/precrime/interfaces/IPreCrime.sol b/contracts/GHST/GHSTOFT/precrime/interfaces/IPreCrime.sol new file mode 100644 index 000000000..43c3d8444 --- /dev/null +++ b/contracts/GHST/GHSTOFT/precrime/interfaces/IPreCrime.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; +struct PreCrimePeer { + uint32 eid; + bytes32 preCrime; + bytes32 oApp; +} + +// TODO not done yet +interface IPreCrime { + error OnlyOffChain(); + + // for simulate() + error PacketOversize(uint256 max, uint256 actual); + error PacketUnsorted(); + error SimulationFailed(bytes reason); + + // for preCrime() + error SimulationResultNotFound(uint32 eid); + error InvalidSimulationResult(uint32 eid, bytes reason); + error CrimeFound(bytes crime); + + function getConfig(bytes[] calldata _packets, uint256[] calldata _packetMsgValues) external returns (bytes memory); + + function simulate( + bytes[] calldata _packets, + uint256[] calldata _packetMsgValues + ) external payable returns (bytes memory); + + function buildSimulationResult() external view returns (bytes memory); + + function preCrime( + bytes[] calldata _packets, + uint256[] calldata _packetMsgValues, + bytes[] calldata _simulations + ) external; + + function version() external view returns (uint64 major, uint8 minor); +} diff --git a/contracts/GHST/GHSTOFT/precrime/libs/Packet.sol b/contracts/GHST/GHSTOFT/precrime/libs/Packet.sol new file mode 100644 index 000000000..dc4dd3927 --- /dev/null +++ b/contracts/GHST/GHSTOFT/precrime/libs/Packet.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; + +/** + * @title InboundPacket + * @dev Structure representing an inbound packet received by the contract. + */ +struct InboundPacket { + Origin origin; // Origin information of the packet. + uint32 dstEid; // Destination endpointId of the packet. + address receiver; // Receiver address for the packet. + bytes32 guid; // Unique identifier of the packet. + uint256 value; // msg.value of the packet. + address executor; // Executor address for the packet. + bytes message; // Message payload of the packet. + bytes extraData; // Additional arbitrary data for the packet. +} + +/** + * @title PacketDecoder + * @dev Library for decoding LayerZero packets. + */ +library PacketDecoder { + using PacketV1Codec for bytes; + + /** + * @dev Decode an inbound packet from the given packet data. + * @param _packet The packet data to decode. + * @return packet An InboundPacket struct representing the decoded packet. + */ + function decode(bytes calldata _packet) internal pure returns (InboundPacket memory packet) { + packet.origin = Origin(_packet.srcEid(), _packet.sender(), _packet.nonce()); + packet.dstEid = _packet.dstEid(); + packet.receiver = _packet.receiverB20(); + packet.guid = _packet.guid(); + packet.message = _packet.message(); + } + + /** + * @dev Decode multiple inbound packets from the given packet data and associated message values. + * @param _packets An array of packet data to decode. + * @param _packetMsgValues An array of associated message values for each packet. + * @return packets An array of InboundPacket structs representing the decoded packets. + */ + function decode( + bytes[] calldata _packets, + uint256[] memory _packetMsgValues + ) internal pure returns (InboundPacket[] memory packets) { + packets = new InboundPacket[](_packets.length); + for (uint256 i = 0; i < _packets.length; i++) { + bytes calldata packet = _packets[i]; + packets[i] = PacketDecoder.decode(packet); + // @dev Allows the verifier to specify the msg.value that gets passed in lzReceive. + packets[i].value = _packetMsgValues[i]; + } + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index f12b6f79f..a555fe98d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -142,6 +142,15 @@ export default { }, }, }, + { + version: "0.8.21", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, { version: "0.4.24", settings: { diff --git a/package.json b/package.json index 924f083f7..69c02b046 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@anders-t/ethers-ledger": "^1.0.4", "@ethersproject/experimental": "^5.4.0", "@ethersproject/hardware-wallets": "github:mudgen/ethers-ledger", + "@layerzerolabs/lz-evm-oapp-v2": "^2.1.13", "@nomiclabs/hardhat-etherscan": "^3.0.0", "@openzeppelin/contracts": "^4.5.0", "@openzeppelin/contracts-upgradeable": "^4.5.2", diff --git a/scripts/deployGHSTAdapter.ts b/scripts/deployGHSTAdapter.ts new file mode 100644 index 000000000..b7c1f6dfe --- /dev/null +++ b/scripts/deployGHSTAdapter.ts @@ -0,0 +1,50 @@ +import { Signer } from "@ethersproject/abstract-signer"; + +import { ethers, network } from "hardhat"; +import { maticDiamondAddress } from "./helperFunctions"; +import { ghstAddress } from "../helpers/constants"; + +async function deployGHSTAdapter() { + const accounts = await ethers.getSigners(); + + let testing = ["hardhat"].includes(network.name); + let signer: Signer; + + let ownerFacet = await ethers.getContractAt( + "OwnershipFacet", + maticDiamondAddress + ); + const owner = await ownerFacet.owner(); + + if (testing) { + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [owner], + }); + signer = await ethers.provider.getSigner(owner); + + //use default signer for base for now + //we are only deploying on polygon + } else if (network.name === "matic") { + signer = accounts[0]; + } else { + throw Error("Incorrect network selected"); + } + + const GHSTOFTAdapter = await ethers.getContractFactory("GHSTOFTAdapter"); + + const polygonLZEndpoint = "0x3c2269811836af69497E5F486A85D7316753cf62"; + + const ghstOftAdapter = await GHSTOFTAdapter.deploy( + ghstAddress, + polygonLZEndpoint, + owner + ); +} + +deployGHSTAdapter() + .then(() => process.exit(1)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployGHSTOFT.ts b/scripts/deployGHSTOFT.ts new file mode 100644 index 000000000..f35811dac --- /dev/null +++ b/scripts/deployGHSTOFT.ts @@ -0,0 +1,53 @@ +import { Signer } from "@ethersproject/abstract-signer"; + +import { ethers, network } from "hardhat"; +import { maticDiamondAddress } from "./helperFunctions"; +import { ghstAddress } from "../helpers/constants"; + +async function deployGHSTOFT() { + const accounts = await ethers.getSigners(); + + let testing = ["hardhat"].includes(network.name); + let signer: Signer; + + let ownerFacet = await ethers.getContractAt( + "OwnershipFacet", + maticDiamondAddress + ); + const owner = await ownerFacet.owner(); + + if (testing) { + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [owner], + }); + signer = await ethers.provider.getSigner(owner); + + //use default signer for base for now + } else if (network.name === "base") { + signer = accounts[0]; + } else { + throw Error("Incorrect network selected"); + } + + const GHSTOFT = await ethers.getContractFactory("GHSTOFT"); + + const baseLZEndpoint = "0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7"; + + const ghstoft = await GHSTOFT.deploy( + "Aavegotchi(GHST) Token", + "GHST", + baseLZEndpoint, + owner + ); + + //after deployment we need to set the ghst contract on polygon as a trusted pair + // await ghstoft.setPeer(109, ghstAddress); +} + +deployGHSTOFT() + .then(() => process.exit(1)) + .catch((error) => { + console.error(error); + process.exit(1); + });