diff --git a/oapp/contracts/oapp/OApp.sol b/oapp/contracts/oapp/OApp.sol new file mode 100644 index 0000000..5ced124 --- /dev/null +++ b/oapp/contracts/oapp/OApp.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { OAppSender } from "./OAppSender.sol"; +// @dev import the origin so its exposed to OApp implementers +import { OAppReceiver, Origin } from "./OAppReceiver.sol"; +import { OAppCore } from "./OAppCore.sol"; + +abstract contract OApp is OAppSender, OAppReceiver { + constructor(address _endpoint, address _owner) OAppCore(_endpoint, _owner) {} + + function oAppVersion() + public + pure + virtual + override(OAppSender, OAppReceiver) + returns (uint64 senderVersion, uint64 receiverVersion) + { + return (SENDER_VERSION, RECEIVER_VERSION); + } +} diff --git a/oapp/contracts/oapp/OAppCore.sol b/oapp/contracts/oapp/OAppCore.sol new file mode 100644 index 0000000..e650cf1 --- /dev/null +++ b/oapp/contracts/oapp/OAppCore.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IOAppCore, ILayerZeroEndpointV2 } from "./interfaces/IOAppCore.sol"; + +abstract contract OAppCore is IOAppCore, Ownable { + ILayerZeroEndpointV2 public immutable endpoint; + mapping(uint32 eid => bytes32 peer) public peers; + + // TODO see if we can move to open zeppelin 5 with ownable(_owner) constructor + constructor(address _endpoint, address _owner) { + _transferOwnership(_owner); + endpoint = ILayerZeroEndpointV2(_endpoint); + endpoint.setDelegate(_owner); // by default, the owner is the delegate + } + + // @dev must-have configurations for standard OApps + function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner { + peers[_eid] = _peer; + emit PeerSet(_eid, _peer); + } + + function _getPeerOrRevert(uint32 _eid) internal view virtual returns (bytes32) { + bytes32 peer = peers[_eid]; + if (peer == bytes32(0)) revert NoPeer(_eid); + return peer; + } + + function setDelegate(address _delegate) public onlyOwner { + endpoint.setDelegate(_delegate); + } +} diff --git a/oapp/contracts/oapp/OAppReceiver.sol b/oapp/contracts/oapp/OAppReceiver.sol new file mode 100644 index 0000000..1451d12 --- /dev/null +++ b/oapp/contracts/oapp/OAppReceiver.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { ILayerZeroReceiver, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroReceiver.sol"; +import { OAppCore } from "./OAppCore.sol"; + +abstract contract OAppReceiver is ILayerZeroReceiver, OAppCore { + error OnlyEndpoint(address addr); + + uint64 internal constant RECEIVER_VERSION = 1; + + function allowInitializePath(Origin calldata origin) public view virtual returns (bool) { + return peers[origin.srcEid] == origin.sender; + } + + /// @dev path nonce starts from 1. if 0 it means that there is no specific nonce enforcement + /// @dev only used to guide the executor actions if the app specify the msg execution to be ordered. + function nextNonce(uint32 /*_srcEid*/, bytes32 /*_sender*/) public view virtual returns (uint64 nonce) { + return 0; + } + + function oAppVersion() public view virtual returns (uint64 senderVersion, uint64 receiverVersion) { + return (0, RECEIVER_VERSION); + } + + // @dev entry point for receiving msg/packet from the endpoint + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) public payable virtual { + if (address(endpoint) != msg.sender) revert OnlyEndpoint(msg.sender); + if (_getPeerOrRevert(_origin.srcEid) != _origin.sender) revert OnlyPeer(_origin.srcEid, _origin.sender); + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + // @dev post basic parameter validation logic implemented in this, must be overriden by OApp + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; +} diff --git a/oapp/contracts/oapp/OAppSender.sol b/oapp/contracts/oapp/OAppSender.sol new file mode 100644 index 0000000..b23975f --- /dev/null +++ b/oapp/contracts/oapp/OAppSender.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +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"; + +abstract contract OAppSender is OAppCore { + using SafeERC20 for IERC20; + + error NotEnoughNative(uint256 msgValue); + error LzTokenUnavailable(); + + uint64 internal constant SENDER_VERSION = 1; + + function oAppVersion() public view virtual returns (uint64 senderVersion, uint64 receiverVersion) { + return (SENDER_VERSION, 0); + } + + /// @dev the generic quote interface to interact with the LayerZero EndpointV2.quote() + 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 generic send interface to interact with the LayerZero EndpointV2.send() + 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 + endpoint.send{ value: messageValue }( + MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0), + _refundAddress + ); + } + + // @dev needs to provide a return value in the event endpoint is nativeErc20 enabled, + // because message value to endpoint would be 0 in that case + // TODO further explanation of how alt token works + function _payNative(uint _nativeFee) internal virtual returns (uint256 nativeFee) { + if (msg.value < _nativeFee) revert NotEnoughNative(msg.value); + return _nativeFee; + } + + function _payLzToken(uint _lzTokenFee) internal virtual { + // @dev cant cache this because it is mutable inside of the endpoint + address lzToken = endpoint.lzToken(); + if (lzToken == address(0x0)) revert LzTokenUnavailable(); + + // @dev pay lzToken fee by sending tokens to the endpoint + IERC20(lzToken).safeTransferFrom(msg.sender, address(endpoint), _lzTokenFee); + } +} diff --git a/oapp/contracts/oapp/examples/ExampleOApp.sol b/oapp/contracts/oapp/examples/ExampleOApp.sol new file mode 100644 index 0000000..76b0889 --- /dev/null +++ b/oapp/contracts/oapp/examples/ExampleOApp.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { OApp, Origin } from "../OApp.sol"; + +contract ExampleOApp is OApp { + constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} + + /// @dev needs to be implemented by the OApp + /// @dev basic security checks are already performed + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + // @dev Do something + } +} diff --git a/oapp/contracts/oapp/examples/ExampleOAppPreCrimeSimulator.sol b/oapp/contracts/oapp/examples/ExampleOAppPreCrimeSimulator.sol new file mode 100644 index 0000000..05adaf7 --- /dev/null +++ b/oapp/contracts/oapp/examples/ExampleOAppPreCrimeSimulator.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { OApp, Origin } from "../OApp.sol"; +import { OAppPreCrimeSimulator } from "../../precrime/OAppPreCrimeSimulator.sol"; + +contract ExampleOAppPreCrimeSimulator is OApp, OAppPreCrimeSimulator { + constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) {} + + /// @dev needs to be implemented by the OApp + /// @dev basic security checks are already performed + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + // @dev Do something + } + + // @dev IF you want preCrime simulator enabled, you need to implement this function as is. + // @dev routes the call down from the OAppPreCrimeSimulator, and up to the OApp + 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 IF you want preCrime enabled, you need to implement this function + function isPeer(uint32 _eid, bytes32 _peer) public view virtual override returns (bool) { + return peers[_eid] != _peer; + } +} diff --git a/oapp/contracts/oapp/examples/ExampleOAppReceiver.sol b/oapp/contracts/oapp/examples/ExampleOAppReceiver.sol new file mode 100644 index 0000000..f135346 --- /dev/null +++ b/oapp/contracts/oapp/examples/ExampleOAppReceiver.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { OAppReceiver, OAppCore, Origin } from "../OAppReceiver.sol"; + +contract ExampleOAppReceiver is OAppReceiver { + constructor(address _endpoint, address _owner) OAppCore(_endpoint, _owner) {} + + /// @dev needs to be implemented by the OApp + /// @dev basic security checks are already performed + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + // @dev Do something + } +} diff --git a/oapp/contracts/oapp/examples/ExampleOAppSender.sol b/oapp/contracts/oapp/examples/ExampleOAppSender.sol new file mode 100644 index 0000000..6ba975b --- /dev/null +++ b/oapp/contracts/oapp/examples/ExampleOAppSender.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { OAppSender, OAppCore } from "../OAppSender.sol"; + +contract ExampleOAppSender is OAppSender { + constructor(address _endpoint, address _owner) OAppCore(_endpoint, _owner) {} +} diff --git a/oapp/contracts/oapp/examples/OmniCounter.sol b/oapp/contracts/oapp/examples/OmniCounter.sol new file mode 100644 index 0000000..17889c7 --- /dev/null +++ b/oapp/contracts/oapp/examples/OmniCounter.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import { ILayerZeroEndpointV2, MessagingFee, MessagingReceipt, Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; + +import { OApp } from "../OApp.sol"; +import { OptionsBuilder } from "../libs/OptionsBuilder.sol"; +import { OAppPreCrimeSimulator } from "../../precrime/OAppPreCrimeSimulator.sol"; + +library MsgCodec { + uint8 internal constant VANILLA_TYPE = 1; + uint8 internal constant COMPOSED_TYPE = 2; + uint8 internal constant ABA_TYPE = 3; + uint8 internal constant COMPOSED_ABA_TYPE = 4; + + uint8 internal constant MSG_TYPE_OFFSET = 0; + uint8 internal constant SRC_EID_OFFSET = 1; + uint8 internal constant VALUE_OFFSET = 5; + + function encode(uint8 _type, uint32 _srcEid) internal pure returns (bytes memory) { + return abi.encodePacked(_type, _srcEid); + } + + function encode(uint8 _type, uint32 _srcEid, uint256 _value) internal pure returns (bytes memory) { + return abi.encodePacked(_type, _srcEid, _value); + } + + function msgType(bytes calldata _message) internal pure returns (uint8) { + return uint8(bytes1(_message[MSG_TYPE_OFFSET:SRC_EID_OFFSET])); + } + + function srcEid(bytes calldata _message) internal pure returns (uint32) { + return uint32(bytes4(_message[SRC_EID_OFFSET:VALUE_OFFSET])); + } + + function value(bytes calldata _message) internal pure returns (uint256) { + return uint256(bytes32(_message[VALUE_OFFSET:])); + } +} + +contract OmniCounter is ILayerZeroComposer, OApp, OAppPreCrimeSimulator { + using MsgCodec for bytes; + using OptionsBuilder for bytes; + + uint256 public count; + uint256 public composedCount; + + address public admin; + uint32 public eid; + + mapping(uint32 srcEid => mapping(bytes32 sender => uint64 nonce)) private maxReceivedNonce; + bool private orderedNonce; + + // for global assertions + mapping(uint32 srcEid => uint256 count) public inboundCount; + mapping(uint32 dstEid => uint256 count) public outboundCount; + + constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) { + admin = msg.sender; + eid = ILayerZeroEndpointV2(_endpoint).eid(); + } + + modifier onlyAdmin() { + require(msg.sender == admin, "only admin"); + _; + } + + // ------------------------------- + // Only Admin + function setAdmin(address _admin) external onlyAdmin { + admin = _admin; + } + + function withdraw(address payable _to, uint256 _amount) external onlyAdmin { + (bool success, ) = _to.call{ value: _amount }(""); + require(success, "OmniCounter: withdraw failed"); + } + + // ------------------------------- + // Send + function increment(uint32 _eid, uint8 _type, bytes calldata _options) external payable { + // bytes memory options = combineOptions(_eid, _type, _options); + _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); + _incrementOutbound(_eid); + } + + // this is a broken function to skip incrementing outbound count + // so that preCrime will fail + function brokenIncrement(uint32 _eid, uint8 _type, bytes calldata _options) external payable onlyAdmin { + // bytes memory options = combineOptions(_eid, _type, _options); + _lzSend(_eid, MsgCodec.encode(_type, eid), _options, MessagingFee(msg.value, 0), payable(msg.sender)); + } + + function batchIncrement( + uint32[] calldata _eids, + uint8[] calldata _types, + bytes[] calldata _options + ) external payable { + require(_eids.length == _options.length && _eids.length == _types.length, "OmniCounter: length mismatch"); + + MessagingReceipt memory receipt; + uint256 providedFee = msg.value; + for (uint256 i = 0; i < _eids.length; i++) { + address refundAddress = i == _eids.length - 1 ? msg.sender : address(this); + uint32 dstEid = _eids[i]; + uint8 msgType = _types[i]; + // bytes memory options = combineOptions(dstEid, msgType, _options[i]); + receipt = _lzSend( + dstEid, + MsgCodec.encode(msgType, eid), + _options[i], + MessagingFee(providedFee, 0), + payable(refundAddress) + ); + _incrementOutbound(dstEid); + providedFee -= receipt.fee.nativeFee; + } + } + + // ------------------------------- + // View + function quote( + uint32 _eid, + uint8 _type, + bytes calldata _options + ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { + // bytes memory options = combineOptions(_eid, _type, _options); + MessagingFee memory fee = _quote(_eid, MsgCodec.encode(_type, eid), _options, false); + return (fee.nativeFee, fee.lzTokenFee); + } + + // @dev enables preCrime simulator + // @dev routes the call down from the OAppPreCrimeSimulator, and up to the OApp + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual override { + _lzReceive(_origin, _guid, _message, _executor, _extraData); + } + + // ------------------------------- + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal override { + _acceptNonce(_origin.srcEid, _origin.sender, _origin.nonce); + uint8 messageType = _message.msgType(); + + if (messageType == MsgCodec.VANILLA_TYPE) { + count++; + + //////////////////////////////// IMPORTANT ////////////////////////////////// + /// if you request for msg.value in the options, you should also encode it + /// into your message and check the value received at destination (example below). + /// if not, the executor could potentially provide less msg.value than you requested + /// leading to unintended behavior. Another option is to assert the executor to be + /// one that you trust. + ///////////////////////////////////////////////////////////////////////////// + require(msg.value >= _message.value(), "OmniCounter: insufficient value"); + + _incrementInbound(_origin.srcEid); + } else if (messageType == MsgCodec.COMPOSED_TYPE || messageType == MsgCodec.COMPOSED_ABA_TYPE) { + count++; + _incrementInbound(_origin.srcEid); + endpoint.sendCompose(address(this), _guid, 0, _message); + } else if (messageType == MsgCodec.ABA_TYPE) { + count++; + _incrementInbound(_origin.srcEid); + + // send back to the sender + _incrementOutbound(_origin.srcEid); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 10); + _lzSend( + _origin.srcEid, + MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid, 10), + options, + MessagingFee(msg.value, 0), + payable(address(this)) + ); + } else { + revert("invalid message type"); + } + } + + function _incrementInbound(uint32 _srcEid) internal { + inboundCount[_srcEid]++; + } + + function _incrementOutbound(uint32 _dstEid) internal { + outboundCount[_dstEid]++; + } + + function lzCompose( + address _oApp, + bytes32 /*_guid*/, + bytes calldata _message, + address, + bytes calldata + ) external payable override { + require(_oApp == address(this), "!oApp"); + require(msg.sender == address(endpoint), "!endpoint"); + + uint8 msgType = _message.msgType(); + if (msgType == MsgCodec.COMPOSED_TYPE) { + composedCount += 1; + } else if (msgType == MsgCodec.COMPOSED_ABA_TYPE) { + composedCount += 1; + + uint32 srcEid = _message.srcEid(); + _incrementOutbound(srcEid); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + _lzSend( + srcEid, + MsgCodec.encode(MsgCodec.VANILLA_TYPE, eid), + options, + MessagingFee(msg.value, 0), + payable(address(this)) + ); + } else { + revert("invalid message type"); + } + } + + // ------------------------------- + // Ordered OApp + // this demonstrates how to build an app that requires execution nonce ordering + // normally an app should decide ordered or not on contract construction + // this is just a demo + function setOrderedNonce(bool _orderedNonce) external onlyOwner { + orderedNonce = _orderedNonce; + } + + function _acceptNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) internal virtual { + uint64 currentNonce = maxReceivedNonce[_srcEid][_sender]; + if (orderedNonce) { + require(_nonce == currentNonce + 1, "OApp: invalid nonce"); + } + // update the max nonce anyway. once the ordered mode is turned on, missing early nonces will be rejected + if (_nonce > currentNonce) { + maxReceivedNonce[_srcEid][_sender] = _nonce; + } + } + + function nextNonce(uint32 _srcEid, bytes32 _sender) public view virtual override returns (uint64) { + if (orderedNonce) { + return maxReceivedNonce[_srcEid][_sender] + 1; + } else { + return 0; // path nonce starts from 1. if 0 it means that there is no specific nonce enforcement + } + } + + // TODO should override oApp version with added ordered nonce increment + // a governance function to skip nonce + function skipInboundNonce(uint32 _srcEid, bytes32 _sender, uint64 _nonce) public virtual onlyOwner { + endpoint.skip(address(this), _srcEid, _sender, _nonce); + if (orderedNonce) { + maxReceivedNonce[_srcEid][_sender]++; + } + } + + function isPeer(uint32 _eid, bytes32 _peer) public view override returns (bool) { + return peers[_eid] == _peer; + } + + // be able to receive ether + receive() external payable virtual {} + + fallback() external payable {} +} diff --git a/oapp/contracts/oapp/examples/OmniCounterPreCrime.sol b/oapp/contracts/oapp/examples/OmniCounterPreCrime.sol new file mode 100644 index 0000000..ae9fa7a --- /dev/null +++ b/oapp/contracts/oapp/examples/OmniCounterPreCrime.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import { PreCrime, PreCrimePeer } from "../../precrime/PreCrime.sol"; +import { InboundPacket } from "../../precrime/libs/Packet.sol"; +import { OmniCounter } from "./OmniCounter.sol"; + +contract OmniCounterPreCrime is PreCrime { + struct ChainCount { + uint32 remoteEid; + uint256 inboundCount; + uint256 outboundCount; + } + + constructor(address _endpoint, address _counter, address _owner) PreCrime(_endpoint, _counter, _owner) {} + + function buildSimulationResult() external view override returns (bytes memory) { + address payable payableSimulator = payable(simulator); + OmniCounter counter = OmniCounter(payableSimulator); + ChainCount[] memory chainCounts = new ChainCount[](preCrimePeers.length); + for (uint256 i = 0; i < preCrimePeers.length; i++) { + uint32 remoteEid = preCrimePeers[i].eid; + chainCounts[i] = ChainCount(remoteEid, counter.inboundCount(remoteEid), counter.outboundCount(remoteEid)); + } + return abi.encode(chainCounts); + } + + function _preCrime( + InboundPacket[] memory /** _packets */, + uint32[] memory _eids, + bytes[] memory _simulations + ) internal view override { + uint32 localEid = _getLocalEid(); + ChainCount[] memory localChainCounts; + + // find local chain counts + for (uint256 i = 0; i < _eids.length; i++) { + if (_eids[i] == localEid) { + localChainCounts = abi.decode(_simulations[i], (ChainCount[])); + break; + } + } + + // local against remote + for (uint256 i = 0; i < _eids.length; i++) { + uint32 remoteEid = _eids[i]; + ChainCount[] memory remoteChainCounts = abi.decode(_simulations[i], (ChainCount[])); + (uint256 _inboundCount, ) = _findChainCounts(localChainCounts, remoteEid); + (, uint256 _outboundCount) = _findChainCounts(remoteChainCounts, localEid); + if (_inboundCount > _outboundCount) { + revert CrimeFound("inboundCount > outboundCount"); + } + } + } + + function _findChainCounts( + ChainCount[] memory _chainCounts, + uint32 _remoteEid + ) internal pure returns (uint256, uint256) { + for (uint256 i = 0; i < _chainCounts.length; i++) { + if (_chainCounts[i].remoteEid == _remoteEid) { + return (_chainCounts[i].inboundCount, _chainCounts[i].outboundCount); + } + } + return (0, 0); + } + + function _getPreCrimePeers( + InboundPacket[] memory _packets + ) internal view override returns (PreCrimePeer[] memory peers) { + PreCrimePeer[] memory allPeers = preCrimePeers; + PreCrimePeer[] memory peersTmp = new PreCrimePeer[](_packets.length); + + int256 cursor = -1; + for (uint256 i = 0; i < _packets.length; i++) { + uint32 srcEid = _packets[i].origin.srcEid; + + // push src eid & peer + int256 index = _indexOf(allPeers, srcEid); + if (index >= 0 && _indexOf(peersTmp, srcEid) < 0) { + cursor++; + peersTmp[uint256(cursor)] = allPeers[uint256(index)]; + } + } + // copy to return + if (cursor >= 0) { + uint256 len = uint256(cursor) + 1; + peers = new PreCrimePeer[](len); + for (uint256 i = 0; i < len; i++) { + peers[i] = peersTmp[i]; + } + } + } + + function _indexOf(PreCrimePeer[] memory _peers, uint32 _eid) internal pure returns (int256) { + for (uint256 i = 0; i < _peers.length; i++) { + if (_peers[i].eid == _eid) return int256(i); + } + return -1; + } +} diff --git a/oapp/contracts/oapp/interfaces/IOAppComposer.sol b/oapp/contracts/oapp/interfaces/IOAppComposer.sol new file mode 100644 index 0000000..75b508a --- /dev/null +++ b/oapp/contracts/oapp/interfaces/IOAppComposer.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { ILayerZeroComposer } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroComposer.sol"; + +// @dev allows developers to inherit just the OApp package, not the protocol as well +interface IOAppComposer is ILayerZeroComposer {} diff --git a/oapp/contracts/oapp/interfaces/IOAppCore.sol b/oapp/contracts/oapp/interfaces/IOAppCore.sol new file mode 100644 index 0000000..36a29cc --- /dev/null +++ b/oapp/contracts/oapp/interfaces/IOAppCore.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { ILayerZeroEndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +interface IOAppCore { + error OnlyPeer(uint32 eid, bytes32 sender); + error NoPeer(uint32 eid); + error InvalidEndpointCall(); + + event PeerSet(uint32 eid, bytes32 peer); + + function oAppVersion() external view returns (uint64 senderVersion, uint64 receiverVersion); + + function endpoint() external view returns (ILayerZeroEndpointV2 iEndpoint); + + function peers(uint32 _eid) external view returns (bytes32 peer); + + function setPeer(uint32 _eid, bytes32 _peer) external; + + function setDelegate(address _delegate) external; +} diff --git a/oapp/contracts/oapp/interfaces/IOAppMsgInspector.sol b/oapp/contracts/oapp/interfaces/IOAppMsgInspector.sol new file mode 100644 index 0000000..9bb0565 --- /dev/null +++ b/oapp/contracts/oapp/interfaces/IOAppMsgInspector.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +interface IOAppMsgInspector { + error InspectionFailed(bytes message, bytes options); + + // @dev allow the inspector to examine contents and optionally throw a revert if either is invalid + function inspect(bytes calldata _message, bytes calldata _options) external view returns (bool valid); +} diff --git a/oapp/contracts/oapp/interfaces/IOAppOptionsType3.sol b/oapp/contracts/oapp/interfaces/IOAppOptionsType3.sol new file mode 100644 index 0000000..2160136 --- /dev/null +++ b/oapp/contracts/oapp/interfaces/IOAppOptionsType3.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +struct EnforcedOptionParam { + uint32 eid; + uint16 msgType; + bytes options; +} + +interface IOAppOptionsType3 { + error InvalidOptions(bytes options); + + event EnforcedOptionSet(EnforcedOptionParam[] _enforcedOptions); + + function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) external; + + function combineOptions( + uint32 _eid, + uint16 _msgType, + bytes calldata _extraOptions + ) external view returns (bytes memory options); +} diff --git a/oapp/contracts/oapp/libs/OAppOptionsType3.sol b/oapp/contracts/oapp/libs/OAppOptionsType3.sol new file mode 100644 index 0000000..701fe26 --- /dev/null +++ b/oapp/contracts/oapp/libs/OAppOptionsType3.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IOAppOptionsType3, EnforcedOptionParam } from "../interfaces/IOAppOptionsType3.sol"; + +abstract contract OAppOptionsType3 is IOAppOptionsType3, Ownable { + uint16 internal constant OPTION_TYPE_3 = 3; + + // @dev These enforced options can vary as the potential options/execution on remote may differ + // eg. Amount of lzReceive gas necessary to deliver a composed message adds overhead you dont want to pay + // if you are only making a standard crosschain call (no deliver compose) + // enforcedOptions[eid][msgType] = enforcedOptions + // The "msgType" should be defined in the child contract + mapping(uint32 eid => mapping(uint16 msgType => bytes enforceOption)) public enforcedOptions; + + function setEnforcedOptions(EnforcedOptionParam[] calldata _enforcedOptions) public virtual onlyOwner { + for (uint256 i = 0; i < _enforcedOptions.length; i++) { + // enforced not supported for options type 1 and 2 + _assertOptionsType3(_enforcedOptions[i].options); + enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options; + } + + emit EnforcedOptionSet(_enforcedOptions); + } + + 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; + + // if caller provided options, it must be type 3 + // remove the first 2 bytes containing the type and combine with enforced + if (_extraOptions.length >= 2) { + _assertOptionsType3(_extraOptions); + return bytes.concat(enforced, _extraOptions[2:]); + } + + // no valid set of options was found + revert InvalidOptions(_extraOptions); + } + + 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/oapp/contracts/oapp/libs/OptionsBuilder.sol b/oapp/contracts/oapp/libs/OptionsBuilder.sol new file mode 100644 index 0000000..6a9d23e --- /dev/null +++ b/oapp/contracts/oapp/libs/OptionsBuilder.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +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"; + +library OptionsBuilder { + using SafeCast for uint256; + using BytesLib for bytes; + + 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; + + error InvalidSize(uint256 max, uint256 actual); + + modifier onlyType3(bytes memory _options) { + require(_options.toUint16(0) == TYPE_3, "OptionsBuilder: invalid options type"); + _; + } + + function newOptions() internal pure returns (bytes memory) { + return abi.encodePacked(TYPE_3); + } + + 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); + } + + 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); + } + + 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); + } + + function addExecutorOrderedExecutionOption( + bytes memory _options + ) internal pure onlyType3(_options) returns (bytes memory) { + return addExecutorOption(_options, ExecutorOptions.OPTION_TYPE_ORDERED_EXECUTION, bytes("")); + } + + function addDVNPreCrimeOption( + bytes memory _options, + uint8 _dvnIdx + ) internal pure onlyType3(_options) returns (bytes memory) { + return addDVNOption(_options, _dvnIdx, DVNOptions.OPTION_TYPE_PRECRIME, bytes("")); + } + + 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 + ); + } + + 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 + ); + } + + 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); + } + + function encodeLegacyOptionsType2( + uint256 _executionGas, + uint256 _amount, + bytes memory _receiver // use bytes instead of bytes32 in legacy type 2 + ) internal pure returns (bytes memory) { + if (_executionGas > type(uint128).max) revert InvalidSize(type(uint128).max, _executionGas); + if (_amount > type(uint128).max) revert InvalidSize(type(uint128).max, _amount); + if (_receiver.length > 32) revert InvalidSize(32, _receiver.length); + return abi.encodePacked(TYPE_2, _executionGas, _amount, _receiver); + } +} diff --git a/oapp/contracts/oft/OFT.sol b/oapp/contracts/oft/OFT.sol new file mode 100644 index 0000000..6d45507 --- /dev/null +++ b/oapp/contracts/oft/OFT.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { OFTCore } from "./OFTCore.sol"; + +contract OFT is OFTCore, ERC20 { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _owner + ) ERC20(_name, _symbol) OFTCore(decimals(), _lzEndpoint, _owner) {} + + // @dev major indicates they shared a compatible msg payload format and CAN communicate between one another + // @dev minor indicates a varying version, eg. OFTAdapter vs. OFT + function oftVersion() external pure returns (uint64 major, uint64 minor) { + return (1, 1); + } + + function token() external view virtual returns (address) { + return address(this); + } + + // @dev burn the tokens from the users specified balance + function _debitSender( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual override returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + (amountDebitedLD, amountToCreditLD) = _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + + _burn(msg.sender, amountDebitedLD); + } + + // @dev burn the tokens that someone has sent into this contract in a push method + // @dev allows anyone to send tokens that have been sent to this contract + // @dev similar to how you can push tokens to the endpoint to pay the msg fee, vs the endpoint needing approval + function _debitThis( + uint256 _minAmountToReceiveLD, + uint32 _dstEid + ) internal virtual override returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + (amountDebitedLD, amountToCreditLD) = _debitView(balanceOf(address(this)), _minAmountToReceiveLD, _dstEid); + + _burn(address(this), amountDebitedLD); + } + + function _credit( + address _to, + uint256 _amountToCreditLD, + uint32 /*_srcEid*/ + ) internal virtual override returns (uint256 amountReceivedLD) { + _mint(_to, _amountToCreditLD); + return _amountToCreditLD; + } +} diff --git a/oapp/contracts/oft/OFTAdapter.sol b/oapp/contracts/oft/OFTAdapter.sol new file mode 100644 index 0000000..52d73cd --- /dev/null +++ b/oapp/contracts/oft/OFTAdapter.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { IERC20Metadata, IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { OFTCore } from "./OFTCore.sol"; + +contract OFTAdapter is OFTCore { + using SafeERC20 for IERC20; + + IERC20 internal immutable innerToken; + + uint256 public outboundAmount; + + constructor( + address _token, + address _lzEndpoint, + address _owner + ) OFTCore(IERC20Metadata(_token).decimals(), _lzEndpoint, _owner) { + innerToken = IERC20(_token); + } + + // todo: why is this 1.2 but OFT is 1.1? + function oftVersion() external pure returns (uint64 major, uint64 minor) { + return (1, 2); + } + + /// @dev note that token() address of OFTAdapter is the address of the innerToken + /// @dev on composing, the composer should assert that oApp is OFTAdapter, not the token + function token() public view virtual returns (address) { + return address(innerToken); + } + + function _debitSender( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual override returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + (amountDebitedLD, amountToCreditLD) = _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + // @dev msg.sender will need to approve this amountLD of tokens to be locked inside of the contract + innerToken.safeTransferFrom(msg.sender, address(this), amountDebitedLD); + + // @dev amountDebited could be 100, with a 10% fee, the credited amount is 90, + // so technically the amountToCredit would be locked as outboundAmount + outboundAmount += amountToCreditLD; + + // @dev will need to override this and do balanceBefore, and balanceAfter IF the innerToken has fees on transfers + } + + // @dev allow a sender to send tokens that are inside of contract, but are NOT accounted for in outboundAmount + function _debitThis( + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual override returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + uint256 availableToSend = innerToken.balanceOf(address(this)) - outboundAmount; + (amountDebitedLD, amountToCreditLD) = _debitView(availableToSend, _minAmountToCreditLD, _dstEid); + // @dev tokens are already in contract so dont need to transferFrom + + outboundAmount += amountToCreditLD; + + // @dev TODO add comments that demonstrate that if you care about the dust, you can refund it via a transfer + // back to the sender or refund address? wont make sense for cost vs. dust amount in most tokens, maybe wbtc? + } + + function _credit( + address _to, + uint256 _amountToCreditLD, + uint32 /*_srcEid*/ + ) internal virtual override returns (uint256 amountReceivedLD) { + outboundAmount -= _amountToCreditLD; + innerToken.safeTransfer(_to, _amountToCreditLD); + return _amountToCreditLD; + } +} diff --git a/oapp/contracts/oft/OFTCore.sol b/oapp/contracts/oft/OFTCore.sol new file mode 100644 index 0000000..b5f390c --- /dev/null +++ b/oapp/contracts/oft/OFTCore.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +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"; + +abstract contract OFTCore is IOFT, OApp, OAppPreCrimeSimulator, OAppOptionsType3 { + using OFTMsgCodec for bytes; + using OFTMsgCodec for bytes32; + + // @dev provides a conversion rate when swapping between denominations in shareDecimals...SD and localDecimals...LD + uint256 public immutable decimalConversionRate; + + // @dev execution types to handle different enforcedOptions + uint16 public constant SEND = 1; + uint16 public constant SEND_AND_CALL = 2; + + // @dev optional interface for an arbitrary contract to inspect both 'message' and 'options' + address public msgInspector; + + constructor(uint8 _localDecimals, address _endpoint, address _owner) OApp(_endpoint, _owner) { + if (_localDecimals < sharedDecimals()) revert InvalidLocalDecimals(); + decimalConversionRate = 10 ** (_localDecimals - sharedDecimals()); + } + + // @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; + } + + function setMsgInspector(address _msgInspector) public virtual onlyOwner { + msgInspector = _msgInspector; + emit MsgInspectorSet(_msgInspector); + } + + function quoteOFT( + SendParam calldata _sendParam, + bytes calldata /*_oftCmd*/ + ) + external + view + virtual + returns (OFTLimit memory oftLimit, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory oftReceipt) + { + uint256 minAmountLD = 0; + uint256 maxAmountLD = type(uint64).max; + oftLimit = OFTLimit(minAmountLD, maxAmountLD); + + // @dev unused in the default implementation, future proofs complex fees inside of an oft send + oftFeeDetails = new OFTFeeDetail[](0); + + (uint256 amountToDebitLD, uint256 amountToCreditLD) = _debitView( + _sendParam.amountToSendLD, + _sendParam.minAmountToCreditLD, + _sendParam.dstEid + ); + oftReceipt = OFTReceipt(amountToDebitLD, amountToCreditLD); + } + + // @dev Requests a nativeFee/lzTokenFee quote for sending the corresponding msg crosschain through the layerZero Endpoint + function quoteSend( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + bool _payInLzToken, + bytes calldata _composeMsg, + bytes calldata /*_oftCmd*/ + ) external view virtual returns (MessagingFee memory msgFee) { + (, uint256 amountToCreditLD) = _debitView( + _sendParam.amountToSendLD, + _sendParam.minAmountToCreditLD, + _sendParam.dstEid + ); + + (bytes memory message, bytes memory options) = _buildMsgAndOptions( + _sendParam, + _extraOptions, + _composeMsg, + amountToCreditLD + ); + + return _quote(_sendParam.dstEid, message, options, _payInLzToken); + } + + // @dev executes a crosschain OFT swap via layerZero Endpoint + function send( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + MessagingFee calldata _fee, + address _refundAddress, + bytes calldata _composeMsg, + bytes calldata /*_oftCmd*/ + ) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) { + (uint256 amountDebitedLD, uint256 amountToCreditLD) = _debit( + _sendParam.amountToSendLD, + _sendParam.minAmountToCreditLD, + _sendParam.dstEid + ); + + (bytes memory message, bytes memory options) = _buildMsgAndOptions( + _sendParam, + _extraOptions, + _composeMsg, + amountToCreditLD + ); + + msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); + oftReceipt = OFTReceipt(amountDebitedLD, amountToCreditLD); + + emit OFTSent(msgReceipt.guid, msg.sender, amountDebitedLD, amountToCreditLD, _composeMsg); + } + + function _buildMsgAndOptions( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + bytes calldata _composeMsg, + uint256 _amountToCreditLD + ) internal view virtual returns (bytes memory message, bytes memory options) { + // @dev Remote chains will want to know the composed function caller. + // @dev composed callers MUST include a non empty bytes payload, + // EVEN if you dont require an arbitrary payload to be sent... eg. '0x01' + bool hasCompose; + (message, hasCompose) = OFTMsgCodec.encode(_sendParam.to, _toSD(_amountToCreditLD), _composeMsg); + uint16 msgType = hasCompose ? SEND_AND_CALL : SEND; + options = combineOptions(_sendParam.dstEid, msgType, _extraOptions); + + // @dev only enforced if explicitly set by the owner + if (msgInspector != address(0)) IOAppMsgInspector(msgInspector).inspect(message, options); + } + + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address /*_executor*/, + bytes calldata /*_extraData*/ + ) internal virtual override { + // @dev sendTo is always a bytes32 as the remote chain initiating the call doesnt know remote chain address size + address toAddress = _message.sendTo().bytes32ToAddress(); + + uint256 amountToCreditLD = _toLD(_message.amountSD()); + uint256 amountReceivedLD = _credit(toAddress, amountToCreditLD, _origin.srcEid); + + if (_message.isComposed()) { + 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 Composed toAddress is the same as the receiver of the oft/tokens + // TODO need to document the index / understand how to use it properly + endpoint.sendCompose(toAddress, _guid, 0, composeMsg); + } + + emit OFTReceived(_guid, toAddress, amountToCreditLD, amountReceivedLD); + } + + // @dev enables preCrime simulator + // @dev routes the call down from the OAppPreCrimeSimulator, and up to the OApp + 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 enables preCrime simulator check for isPeer + function isPeer(uint32 _eid, bytes32 _peer) public view virtual override returns (bool) { + return peers[_eid] == _peer; + } + + function _removeDust(uint256 _amountLD) internal view virtual returns (uint256 amountLD) { + return (_amountLD / decimalConversionRate) * decimalConversionRate; + } + + function _toLD(uint64 _amountSD) internal view virtual returns (uint256 amountLD) { + return _amountSD * decimalConversionRate; + } + + function _toSD(uint256 _amountLD) internal view virtual returns (uint64 amountSD) { + return uint64(_amountLD / decimalConversionRate); + } + + // @dev allows the quote functions to mock sending the actual values that would be sent in a send() + function _debitView( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 /*_dstEid*/ + ) internal view virtual returns (uint256 amountToDebitLD, uint256 amountToCreditLD) { + amountToDebitLD = _removeDust(_amountToSendLD); + amountToCreditLD = amountToDebitLD; // these are the same because fees etc. arent being applied + + if (amountToCreditLD < _minAmountToCreditLD) { + revert SlippageExceeded(amountToCreditLD, _minAmountToCreditLD); + } + } + + function _debit( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + if (_amountToSendLD > 0) { + (amountDebitedLD, amountToCreditLD) = _debitSender(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } else { + (amountDebitedLD, amountToCreditLD) = _debitThis(_minAmountToCreditLD, _dstEid); + } + } + + function _debitSender( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual returns (uint256 amountDebitedLD, uint256 amountToCreditLD); + + function _debitThis( + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) internal virtual returns (uint256 amountDebitedLD, uint256 amountToCreditLD); + + function _credit( + address _to, + uint256 _amountToCreditLD, + uint32 _srcEid + ) internal virtual returns (uint256 amountReceivedLD); +} diff --git a/oapp/contracts/oft/OFTPrecrime.sol b/oapp/contracts/oft/OFTPrecrime.sol new file mode 100644 index 0000000..257f1da --- /dev/null +++ b/oapp/contracts/oft/OFTPrecrime.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +// import { IOApp } from "../../interfaces/IOApp.sol"; +// import { IOFT } from "./interfaces/IOFT.sol"; +// import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import { OFTAdapter } from "./OFTAdapter.sol"; + +//contract OFTPreCrime is PreCrime { +// address public oft; +// uint256 public EXPECTED_GLOBAL_SUPPLY; +// +// constructor(address _endpoint, address _oft) PreCrime(_endpoint) { +// oft = _oft; +// } +// +// struct SimulationResult { +// uint256 totalSupplyLD; +// bool isAdapter; +// } +// +// // @dev only necessary when its exclusive 'OFT', NOT 'OFTAdapter' type +// // sum of all tokens in the oft network, it can change, but this will need to be updated for pre-Crime to pass +// function setGlobalSupply(uint256 _globalSupply) public onlyPreCrimeAdmin { +// EXPECTED_GLOBAL_SUPPLY = _globalSupply; +// } +// +// // ------------------------------- +// // PreCrime +// function _receiver() internal view override returns (address) { +// return address(oft); +// } +// +// function _preCrime(bytes[] memory _simulation) internal view override returns (uint16 code, bytes memory reason) { +// uint256 globalSupply; +// uint256 expectedGlobalSupply = EXPECTED_GLOBAL_SUPPLY; +// +// // @dev indicates that there is an 'OFTAdapter' on one of the chains, not necessarily this local chain +// bool isOFTAdapter; +// +// for (uint256 i = 0; i < _simulation.length; i++) { +// SimulationResult memory result = abi.decode(_simulation[i], (SimulationResult)); +// +// if (result.isAdapter) { +// // @dev does not support multiple' 'OFTAdapter' contracts for a given oft mesh +// if (isOFTAdapter) return (CODE_PRECRIME_FAILURE, "OFTPreCrime: multiple OFTAdapters found"); +// isOFTAdapter = true; +// +// expectedGlobalSupply = result.totalSupplyLD; +// } else { +// globalSupply += result.totalSupplyLD; +// } +// } +// +// if (isOFTAdapter && globalSupply > expectedGlobalSupply) { +// // @dev expectedGlobal supply for an 'OFTAdapter' can be slightly higher due to users sending tokens direct +// // to the OFTAdapter contract, cant check explicitly "==" +// return (CODE_PRECRIME_FAILURE, "OFTPreCrime: globalSupply > expectedGlobalSupply"); +// } else if (globalSupply != expectedGlobalSupply) { +// // @dev exclusively 'OFT', NOT 'OFTAdapter' instances, balances should be exactly "==" +// return (CODE_PRECRIME_FAILURE, "OFTPreCrime: globalSupply != expectedGlobalSupply"); +// } else { +// return (CODE_SUCCESS, ""); +// } +// } +// +// function simulationCallback() external view override returns (bytes memory result) { +// address token = IOFT(oft).token(); +// +// // @dev checks if the corresponding _oft on this chain is an adapter version, or returns false if its regular 'OFT' +// // eg. 'OFTAdapter' lock/unlock tokens from an external token contract, vs. regular 'OFT' mints/burns +// bool isAdapter = token != oft; +// +// // @dev for 'OFTAdapter' the total supply is the total amount locked, otherwise its the totalSupply of oft tokens on the chain +// uint256 totalSupply = isAdapter ? IERC20(token).balanceOf(oft) : IERC20(oft).totalSupply(); +// +// return abi.encode(SimulationResult(totalSupply, isAdapter)); +// } +// +// function _simulate(Packet[] calldata _packets) internal override returns (uint16 code, bytes memory simulation) { +// (bool success, bytes memory result) = oft.call{value: msg.value}( +// abi.encodeWithSelector(IOApp.lzReceiveAndRevert.selector, _packets) +// ); +// require(!success, "OFTPreCrime: simulationCallback should be called via revert"); +// +// (, result) = _parseRevertResult(result, LzReceiveRevert.selector); +// return (CODE_SUCCESS, result); +// } +// +// // @dev need to ensure that all preCrimePeers are present inside of the results passed into _checkResultsCompleteness() +// // when checking oft preCrime we always want every simulation/result from the remote peers +// function _getPreCrimePeers( +// Packet[] calldata /*_packets*/ +// ) internal view override returns (uint32[] memory eids, bytes32[] memory peers) { +// // @dev assumes that the preCrimeEids is the full list of oft eids for this oft mesh +// return (preCrimeEids, preCrimePeers); +// } +//} diff --git a/oapp/contracts/oft/extensions/Fee.sol b/oapp/contracts/oft/extensions/Fee.sol new file mode 100644 index 0000000..9eb9287 --- /dev/null +++ b/oapp/contracts/oft/extensions/Fee.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +// TODO not finished yet +abstract contract Fee is Ownable { + uint256 public constant BP_DENOMINATOR = 10000; + + mapping(uint32 => FeeConfig) public dstEidToFeeBps; + uint16 public defaultFeeBp; + address public feeOwner; // defaults to owner + // bool public feesEnabled; + + struct FeeConfig { + uint16 feeBP; + bool enabled; + } + + event SetFeeBp(uint32 dstEid, bool enabled, uint16 feeBp); + event SetDefaultFeeBp(uint16 feeBp); + event SetFeeOwner(address feeOwner); + event SetFeesEnabled(bool isEnabled); + + constructor() { + feeOwner = owner(); + } + + // function setFeesEnabled(bool _isEnabled) public virtual onlyOwner { + // feesEnabled = _isEnabled; + // emit SetFeesEnabled(feesEnabled); + // } + + function setDefaultFeeBp(uint16 _feeBp) public virtual onlyOwner { + require(_feeBp <= BP_DENOMINATOR, "Fee: fee bp must be <= BP_DENOMINATOR"); + defaultFeeBp = _feeBp; + emit SetDefaultFeeBp(defaultFeeBp); + } + + function setFeeBp(uint32 _dstEid, bool _enabled, uint16 _feeBp) public virtual onlyOwner { + require(_feeBp <= BP_DENOMINATOR, "Fee: fee bp must be <= BP_DENOMINATOR"); + dstEidToFeeBps[_dstEid] = FeeConfig(_feeBp, _enabled); + emit SetFeeBp(_dstEid, _enabled, _feeBp); + } + + function setFeeOwner(address _feeOwner) public virtual onlyOwner { + require(_feeOwner != address(0x0), "Fee: feeOwner cannot be 0x"); + feeOwner = _feeOwner; + emit SetFeeOwner(_feeOwner); + } + + function quoteOFTFee(uint32 _dstEid, uint256 _amount) public view virtual returns (uint256 fee) { + FeeConfig memory config = dstEidToFeeBps[_dstEid]; + if (config.enabled) { + fee = (_amount * config.feeBP) / BP_DENOMINATOR; + } else if (defaultFeeBp > 0) { + fee = (_amount * defaultFeeBp) / BP_DENOMINATOR; + } else { + fee = 0; + } + } +} diff --git a/oapp/contracts/oft/interfaces/IOFT.sol b/oapp/contracts/oft/interfaces/IOFT.sol new file mode 100644 index 0000000..f3121ff --- /dev/null +++ b/oapp/contracts/oft/interfaces/IOFT.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { MessagingReceipt, MessagingFee } from "../../oapp/OAppSender.sol"; + +struct SendParam { + uint32 dstEid; + bytes32 to; + uint256 amountToSendLD; + uint256 minAmountToCreditLD; +} + +struct OFTLimit { + uint256 minAmountLD; + uint256 maxAmountLD; +} + +struct OFTReceipt { + uint256 amountDebitLD; + uint256 amountCreditLD; +} + +struct OFTFeeDetail { + uint256 feeAmountLD; + string description; +} + +// @dev does not inherit ERC20 because the OFTAdapter needs to use this interface as well +interface IOFT { + error InvalidLocalDecimals(); + error SlippageExceeded(uint256 amountToCreditLD, uint256 minAmountToCreditLD); + + event MsgInspectorSet(address inspector); + event OFTSent( + bytes32 indexed guid, + address indexed fromAddress, + uint256 amountDebitedLD, + uint256 amountToCreditLD, + bytes composeMsg + ); + event OFTReceived( + bytes32 indexed guid, + address indexed toAddress, + uint256 amountToCreditLD, + uint256 amountReceivedLD + ); + + function oftVersion() external view returns (uint64 major, uint64 minor); + + function token() external view returns (address); + + function sharedDecimals() external view returns (uint8); + + function setMsgInspector(address _msgInspector) external; + + function msgInspector() external view returns (address); + + function quoteOFT( + SendParam calldata _sendParam, + bytes calldata _oftCmd + ) external view returns (OFTLimit memory, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory); + + function quoteSend( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + bool _payInLzToken, + bytes calldata _composeMsg, + bytes calldata _oftCmd + ) external view returns (MessagingFee memory); + + function send( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + MessagingFee calldata _fee, + address _refundAddress, + bytes calldata _composeMsg, + bytes calldata _oftCmd + ) external payable returns (MessagingReceipt memory, OFTReceipt memory); +} diff --git a/oapp/contracts/oft/libs/OFTComposeMsgCodec.sol b/oapp/contracts/oft/libs/OFTComposeMsgCodec.sol new file mode 100644 index 0000000..84c28cd --- /dev/null +++ b/oapp/contracts/oft/libs/OFTComposeMsgCodec.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +library OFTComposeMsgCodec { + 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; + + 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); + } + + function nonce(bytes calldata _msg) internal pure returns (uint64) { + return uint64(bytes8(_msg[:NONCE_OFFSET])); + } + + function srcEid(bytes calldata _msg) internal pure returns (uint32) { + return uint32(bytes4(_msg[NONCE_OFFSET:SRC_EID_OFFSET])); + } + + function amountLD(bytes calldata _msg) internal pure returns (uint256) { + return uint256(bytes32(_msg[SRC_EID_OFFSET:AMOUNT_LD_OFFSET])); + } + + function composeFrom(bytes calldata _msg) internal pure returns (bytes32) { + return bytes32(_msg[AMOUNT_LD_OFFSET:COMPOSE_FROM_OFFSET]); + } + + function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { + return _msg[COMPOSE_FROM_OFFSET:]; + } + + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + function bytes32ToAddress(bytes32 _b) internal pure returns (address) { + return address(uint160(uint256(_b))); + } +} diff --git a/oapp/contracts/oft/libs/OFTMsgCodec.sol b/oapp/contracts/oft/libs/OFTMsgCodec.sol new file mode 100644 index 0000000..ac820f6 --- /dev/null +++ b/oapp/contracts/oft/libs/OFTMsgCodec.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +library OFTMsgCodec { + uint8 private constant SEND_TO_OFFSET = 32; + uint8 private constant SEND_AMOUNT_SD_OFFSET = 40; + + function encode( + bytes32 _sendTo, + uint64 _amountShared, + bytes memory _composeMsg + ) internal view returns (bytes memory _msg, bool hasCompose) { + hasCompose = _composeMsg.length > 0; + _msg = hasCompose + ? abi.encodePacked(_sendTo, _amountShared, addressToBytes32(msg.sender), _composeMsg) + : abi.encodePacked(_sendTo, _amountShared); + } + + function isComposed(bytes calldata _msg) internal pure returns (bool) { + return _msg.length > SEND_AMOUNT_SD_OFFSET; + } + + function sendTo(bytes calldata _msg) internal pure returns (bytes32) { + return bytes32(_msg[:SEND_TO_OFFSET]); + } + + function amountSD(bytes calldata _msg) internal pure returns (uint64) { + return uint64(bytes8(_msg[SEND_TO_OFFSET:SEND_AMOUNT_SD_OFFSET])); + } + + function composeMsg(bytes calldata _msg) internal pure returns (bytes memory) { + return _msg[SEND_AMOUNT_SD_OFFSET:]; + } + + // todo: duplicated codes + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + function bytes32ToAddress(bytes32 _b) internal pure returns (address) { + return address(uint160(uint256(_b))); + } +} diff --git a/oapp/contracts/precrime/OAppPreCrimeSimulator.sol b/oapp/contracts/precrime/OAppPreCrimeSimulator.sol new file mode 100644 index 0000000..d342ce6 --- /dev/null +++ b/oapp/contracts/precrime/OAppPreCrimeSimulator.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IPreCrime } from "./interfaces/IPreCrime.sol"; +import { IOAppPreCrimeSimulator, InboundPacket, Origin } from "./interfaces/IOAppPreCrimeSimulator.sol"; + +abstract contract OAppPreCrimeSimulator is IOAppPreCrimeSimulator, Ownable { + address public preCrime; + + /// @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, can upgrade the preCrime implementation overtime + function setPreCrime(address _preCrime) public virtual onlyOwner { + preCrime = _preCrime; + emit PreCrimeSet(_preCrime); + } + + /// @dev interface for preCrime simulations, ALWAYS reverts at the end with the simulation results + 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 peers/trusted + 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 this removes assembly return 0 callstack exit + this.lzReceiveSimulate{ value: packet.value }( + packet.origin, + packet.guid, + packet.message, + packet.executor, + packet.extraData + ); + } + + // 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 to reset the call stack for 'internal' calls + function lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable virtual { + if (msg.sender != address(this)) revert OnlySelf(); + _lzReceiveSimulate(_origin, _guid, _message, _executor, _extraData); + } + + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal virtual; + + function isPeer(uint32 _eid, bytes32 _peer) public view virtual returns (bool); +} diff --git a/oapp/contracts/precrime/PreCrime.sol b/oapp/contracts/precrime/PreCrime.sol new file mode 100644 index 0000000..b1cddcb --- /dev/null +++ b/oapp/contracts/precrime/PreCrime.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +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, address _owner) { + _transferOwnership(_owner); + 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/oapp/contracts/precrime/extensions/PreCrimeE1.sol b/oapp/contracts/precrime/extensions/PreCrimeE1.sol new file mode 100644 index 0000000..0052534 --- /dev/null +++ b/oapp/contracts/precrime/extensions/PreCrimeE1.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.22; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { ILayerZeroEndpoint } from "@layerzerolabs/lz-evm-v1-0.7/contracts/interfaces/ILayerZeroEndpoint.sol"; + +import { PreCrime } from "../PreCrime.sol"; + +abstract contract PreCrimeE1 is PreCrime { + using SafeCast for uint32; + + uint32 internal immutable localEid; + + constructor(uint32 _localEid, address _endpoint, address _simulator) PreCrime(_endpoint, _simulator, msg.sender) { + localEid = _localEid; + } + + function _getLocalEid() internal view override returns (uint32) { + return localEid; + } + + function _getInboundNonce(uint32 _srcEid, bytes32 _sender) internal view override returns (uint64) { + bytes memory path = _getPath(_srcEid, _sender); + return ILayerZeroEndpoint(lzEndpoint).getInboundNonce(_srcEid.toUint16(), path); + } + + function _getPath(uint32 _srcEid, bytes32 _sender) internal view virtual returns (bytes memory); +} diff --git a/oapp/contracts/precrime/interfaces/IOAppPreCrimeSimulator.sol b/oapp/contracts/precrime/interfaces/IOAppPreCrimeSimulator.sol new file mode 100644 index 0000000..406ccc2 --- /dev/null +++ b/oapp/contracts/precrime/interfaces/IOAppPreCrimeSimulator.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { InboundPacket, Origin } from "../libs/Packet.sol"; + +interface IOAppPreCrimeSimulator { + // @dev simulation result used in PreCrime implementation + error SimulationResult(bytes result); + error OnlySelf(); + + event PreCrimeSet(address preCrimeAddress); + + function preCrime() external view returns (address); + + function oApp() external view returns (address); + + /// @dev sets the preCrime contract, can upgrade the preCrime implementation overtime + function setPreCrime(address _preCrime) external; + + // @dev mocks receiving a packet, then reverts with a series of data to infer the state/result + function lzReceiveAndRevert(InboundPacket[] calldata _packets) external payable; + + function isPeer(uint32 _eid, bytes32 _peer) external view returns (bool); +} diff --git a/oapp/contracts/precrime/interfaces/IPreCrime.sol b/oapp/contracts/precrime/interfaces/IPreCrime.sol new file mode 100644 index 0000000..b0ac424 --- /dev/null +++ b/oapp/contracts/precrime/interfaces/IPreCrime.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; +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/oapp/contracts/precrime/libs/Packet.sol b/oapp/contracts/precrime/libs/Packet.sol new file mode 100644 index 0000000..b239da5 --- /dev/null +++ b/oapp/contracts/precrime/libs/Packet.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +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"; + +struct InboundPacket { + Origin origin; + uint32 dstEid; + address receiver; + bytes32 guid; + uint256 value; + address executor; + bytes message; + bytes extraData; +} + +library PacketDecoder { + using PacketV1Codec for bytes; + + 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(); + } + + 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/oapp/foundry.toml b/oapp/foundry.toml new file mode 100644 index 0000000..2017f2a --- /dev/null +++ b/oapp/foundry.toml @@ -0,0 +1,32 @@ +[profile.default] +solc = '0.8.22' +verbosity = 3 +src = "contracts" +test = "test" +out = "out" +cache_path = "cache" +optimizer = true +optimizer_runs = 20_000 + +allow_paths = [ + "../.yarn/unplugged", + "../protocol", + "../messagelib" +] + +libs = [ + '../lib', +] + +remappings = [ + # note: map to package level only, required for pnp-berry to work with foundry + # ok - solidity-stringutils/=node_modules/solidity-stringutils/ + # not ok - solidity-stringutils/=node_modules/solidity-stringutils/src/ + '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', + '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', + 'solidity-bytes-utils/=node_modules/solidity-bytes-utils/', + 'hardhat-deploy/=node_modules/hardhat-deploy/', + '@layerzerolabs/lz-evm-protocol-v2/=node_modules/@layerzerolabs/lz-evm-protocol-v2/', + '@layerzerolabs/lz-evm-messagelib-v2/=node_modules/@layerzerolabs/lz-evm-messagelib-v2/', + '@layerzerolabs/lz-evm-v1-0.7/=node_modules/@layerzerolabs/lz-evm-v1-0.7/' +] \ No newline at end of file diff --git a/oapp/package.json b/oapp/package.json new file mode 100644 index 0000000..ae6dfac --- /dev/null +++ b/oapp/package.json @@ -0,0 +1,36 @@ +{ + "name": "@layerzerolabs/lz-evm-oapp-v2", + "version": "1.5.78", + "license": "BUSL-1.1", + "files": [ + "contracts/**" + ], + "scripts": { + "clean": "rimraf cache out", + "build": "forge build", + "test": "forge test" + }, + "dependencies": { + "@layerzerolabs/lz-evm-messagelib-v2": "workspace:^", + "@layerzerolabs/lz-evm-protocol-v2": "workspace:^", + "@layerzerolabs/lz-evm-v1-0.7": "^1.5.77" + }, + "devDependencies": { + "@openzeppelin/contracts": "^4.8.1", + "@openzeppelin/contracts-upgradeable": "^4.8.1", + "hardhat-deploy": "^0.11.44", + "rimraf": "^5.0.5", + "solidity-bytes-utils": "^0.8.0" + }, + "peerDependencies": { + "solidity-bytes-utils": "^0.8.0" + }, + "publishConfig": { + "access": "restricted" + }, + "lzVersions": { + "default": [ + "v2" + ] + } +} diff --git a/oapp/test/OFT.t.sol b/oapp/test/OFT.t.sol new file mode 100644 index 0000000..f364501 --- /dev/null +++ b/oapp/test/OFT.t.sol @@ -0,0 +1,588 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { OptionsBuilder } from "../contracts/oapp/libs/OptionsBuilder.sol"; + +import { OFTMock } from "./mocks/OFTMock.sol"; +import { MessagingFee, MessagingReceipt } from "../contracts/oft/OFTCore.sol"; +import { OFTAdapterMock } from "./mocks/OFTAdapterMock.sol"; +import { ERC20Mock } from "./mocks/ERC20Mock.sol"; +import { OFTComposerMock } from "./mocks/OFTComposerMock.sol"; +import { OFTInspectorMock, IOAppMsgInspector } from "./mocks/OFTInspectorMock.sol"; +import { IOAppOptionsType3, OAppOptionsType3, EnforcedOptionParam } from "../contracts/oapp/libs/OAppOptionsType3.sol"; + +import { OFTMsgCodec } from "../contracts/oft/libs/OFTMsgCodec.sol"; +import { OFTComposeMsgCodec } from "../contracts/oft/libs/OFTComposeMsgCodec.sol"; + +import { IOFT, SendParam, OFTLimit, OFTReceipt } from "../contracts/oft/interfaces/IOFT.sol"; +import { IERC20Metadata, IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { TestHelper } from "./TestHelper.sol"; + +import "forge-std/console.sol"; + +contract OFTTest is TestHelper { + using OptionsBuilder for bytes; + + uint32 aEid = 1; + uint32 bEid = 2; + uint32 cEid = 3; + + OFTMock aOFT; + OFTMock bOFT; + OFTAdapterMock cOFTAdapter; + ERC20Mock cERC20Mock; + + OFTInspectorMock oAppInspector; + + address public userA = address(0x1); + address public userB = address(0x2); + address public userC = address(0x3); + uint256 public initialBalance = 100 ether; + + function setUp() public virtual override { + vm.deal(userA, 1000 ether); + vm.deal(userB, 1000 ether); + vm.deal(userC, 1000 ether); + + super.setUp(); + setUpEndpoints(3, LibraryType.UltraLightNode); + + aOFT = OFTMock( + _deployOApp(type(OFTMock).creationCode, abi.encode("aOFT", "aOFT", address(endpoints[aEid]), address(this))) + ); + + bOFT = OFTMock( + _deployOApp(type(OFTMock).creationCode, abi.encode("bOFT", "bOFT", address(endpoints[bEid]), address(this))) + ); + + cERC20Mock = new ERC20Mock("cToken", "cToken"); + cOFTAdapter = OFTAdapterMock( + _deployOApp( + type(OFTAdapterMock).creationCode, + abi.encode(address(cERC20Mock), address(endpoints[cEid]), address(this)) + ) + ); + + // config and wire the ofts + address[] memory ofts = new address[](3); + ofts[0] = address(aOFT); + ofts[1] = address(bOFT); + ofts[2] = address(cOFTAdapter); + this.wireOApps(ofts); + + // mint tokens + aOFT.mint(userA, initialBalance); + bOFT.mint(userB, initialBalance); + cERC20Mock.mint(userC, initialBalance); + + // deploy a universal inspector, can be used by each oft + oAppInspector = new OFTInspectorMock(); + } + + function test_constructor() public { + assertEq(aOFT.owner(), address(this)); + assertEq(bOFT.owner(), address(this)); + assertEq(cOFTAdapter.owner(), address(this)); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + assertEq(IERC20(cOFTAdapter.token()).balanceOf(userC), initialBalance); + + assertEq(aOFT.token(), address(aOFT)); + assertEq(bOFT.token(), address(bOFT)); + assertEq(cOFTAdapter.token(), address(cERC20Mock)); + } + + function test_send_oft() public { + uint256 tokensToSend = 1 ether; + SendParam memory sendParam = SendParam(bEid, addressToBytes32(userB), tokensToSend, tokensToSend); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + MessagingFee memory fee = aOFT.quoteSend(sendParam, options, false, "", ""); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(userB), initialBalance); + + vm.prank(userA); + aOFT.send{ value: fee.nativeFee }(sendParam, options, fee, payable(address(this)), "", ""); + verifyPackets(bEid, addressToBytes32(address(bOFT))); + + assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); + assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend); + } + + function test_send_oft_compose_msg() public { + uint256 tokensToSend = 1 ether; + + OFTComposerMock composer = new OFTComposerMock(); + + SendParam memory sendParam = SendParam(bEid, addressToBytes32(address(composer)), tokensToSend, tokensToSend); + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorLzComposeOption(0, 500000, 0); + bytes memory composeMsg = hex"1234"; + MessagingFee memory fee = aOFT.quoteSend(sendParam, options, false, composeMsg, ""); + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(bOFT.balanceOf(address(composer)), 0); + + vm.prank(userA); + (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = aOFT.send{ value: fee.nativeFee }( + sendParam, + options, + fee, + payable(address(this)), + composeMsg, + "" + ); + verifyPackets(bEid, addressToBytes32(address(bOFT))); + + // lzCompose params + uint32 dstEid_ = bEid; + address from_ = address(bOFT); + bytes memory options_ = options; + bytes32 guid_ = msgReceipt.guid; + address to_ = address(composer); + bytes memory composerMsg_ = OFTComposeMsgCodec.encode( + msgReceipt.nonce, + aEid, + oftReceipt.amountCreditLD, + abi.encodePacked(addressToBytes32(userA), composeMsg) + ); + this.lzCompose(dstEid_, from_, options_, guid_, to_, composerMsg_); + + assertEq(aOFT.balanceOf(userA), initialBalance - tokensToSend); + assertEq(bOFT.balanceOf(address(composer)), tokensToSend); + + assertEq(composer.from(), from_); + assertEq(composer.guid(), guid_); + assertEq(composer.message(), composerMsg_); + assertEq(composer.executor(), address(this)); + assertEq(composer.extraData(), composerMsg_); // default to setting the extraData to the message as well to test + } + + function test_oft_compose_codec() public { + uint64 nonce = 1; + uint32 srcEid = 2; + uint256 amountCreditLD = 3; + bytes memory composeMsg = hex"1234"; + + bytes memory message = OFTComposeMsgCodec.encode( + nonce, + srcEid, + amountCreditLD, + abi.encodePacked(addressToBytes32(msg.sender), composeMsg) + ); + (uint64 nonce_, uint32 srcEid_, uint256 amountCreditLD_, bytes32 composeFrom_, bytes memory composeMsg_) = this + .decodeOFTComposeMsgCodec(message); + + assertEq(nonce_, nonce); + assertEq(srcEid_, srcEid); + assertEq(amountCreditLD_, amountCreditLD); + assertEq(composeFrom_, addressToBytes32(msg.sender)); + assertEq(composeMsg_, composeMsg); + } + + function decodeOFTComposeMsgCodec( + bytes calldata message + ) + public + pure + returns (uint64 nonce, uint32 srcEid, uint256 amountCreditLD, bytes32 composeFrom, bytes memory composeMsg) + { + nonce = OFTComposeMsgCodec.nonce(message); + srcEid = OFTComposeMsgCodec.srcEid(message); + amountCreditLD = OFTComposeMsgCodec.amountLD(message); + composeFrom = OFTComposeMsgCodec.composeFrom(message); + composeMsg = OFTComposeMsgCodec.composeMsg(message); + } + + function test_debit_slippage_removeDust() public { + uint256 amountToSendLD = 1.23456789 ether; + uint256 minAmountToCreditLD = 1.23456789 ether; + uint32 dstEid = aEid; + + // remove the dust form the shared decimal conversion + assertEq(aOFT.removeDust(amountToSendLD), 1.234567 ether); + + vm.expectRevert( + abi.encodeWithSelector(IOFT.SlippageExceeded.selector, aOFT.removeDust(amountToSendLD), minAmountToCreditLD) + ); + aOFT.debit(amountToSendLD, minAmountToCreditLD, dstEid); + } + + function test_debit_slippage_minAmountToCreditLD() public { + uint256 amountToSendLD = 1 ether; + uint256 minAmountToCreditLD = 1.00000001 ether; + uint32 dstEid = aEid; + + vm.expectRevert(abi.encodeWithSelector(IOFT.SlippageExceeded.selector, amountToSendLD, minAmountToCreditLD)); + aOFT.debit(amountToSendLD, minAmountToCreditLD, dstEid); + } + + function test_toLD() public { + uint64 amountSD = 1000; + assertEq(amountSD * aOFT.decimalConversionRate(), aOFT.toLD(uint64(amountSD))); + } + + function test_toSD() public { + uint256 amountLD = 1000000; + assertEq(amountLD / aOFT.decimalConversionRate(), aOFT.toSD(amountLD)); + } + + function test_oft_debit_sender() public { + uint256 amountToSendLD = 1 ether; + uint256 minAmountToCreditLD = 1 ether; + uint32 dstEid = aEid; + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(aOFT.balanceOf(address(this)), 0); + + vm.prank(userA); + (uint256 amountDebitedLD, uint256 amountToCreditLD) = aOFT.debit(amountToSendLD, minAmountToCreditLD, dstEid); + + assertEq(amountDebitedLD, amountToSendLD); + assertEq(amountToCreditLD, amountToSendLD); + + assertEq(aOFT.balanceOf(userA), initialBalance - amountToSendLD); + assertEq(aOFT.balanceOf(address(this)), 0); + } + + function test_oft_debit_this() public { + uint256 amountToSendLD = 1 ether; + uint256 minAmountToCreditLD = 1 ether; + uint32 dstEid = aEid; + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(aOFT.balanceOf(address(this)), 0); + + vm.prank(userA); + aOFT.transfer(address(aOFT), amountToSendLD); + assertEq(aOFT.balanceOf(userA), initialBalance - amountToSendLD); + assertEq(aOFT.balanceOf(address(aOFT)), amountToSendLD); + + // reverts if a user tries to spend the tokens via debitThis AND the minimum exceeds the balance inside contract + vm.prank(userB); + vm.expectRevert( + abi.encodeWithSelector(IOFT.SlippageExceeded.selector, amountToSendLD, minAmountToCreditLD + 1) + ); + aOFT.debit(amountToSendLD, minAmountToCreditLD + 1, dstEid); + assertEq(aOFT.balanceOf(address(aOFT)), amountToSendLD); + + // Someone else can spend the tokens the user sent into the contract + vm.prank(userB); + (uint256 amountDebitedLD, uint256 amountToCreditLD) = aOFT.debit(0, minAmountToCreditLD, dstEid); + + assertEq(amountDebitedLD, amountToSendLD); + assertEq(amountToCreditLD, amountToSendLD); + + assertEq(aOFT.balanceOf(userA), initialBalance - amountToSendLD); + assertEq(aOFT.balanceOf(address(this)), 0); + assertEq(aOFT.balanceOf(address(aOFT)), 0); + } + + function test_oft_credit() public { + uint256 amountToCreditLD = 1 ether; + uint32 srcEid = aEid; + + assertEq(aOFT.balanceOf(userA), initialBalance); + assertEq(aOFT.balanceOf(address(this)), 0); + + vm.prank(userA); + uint256 amountReceived = aOFT.credit(userA, amountToCreditLD, srcEid); + + assertEq(aOFT.balanceOf(userA), initialBalance + amountReceived); + assertEq(aOFT.balanceOf(address(this)), 0); + } + + function test_oft_adapter_debit_sender() public { + uint256 amountToSendLD = 1 ether; + uint256 minAmountToCreditLD = 1 ether; + uint32 dstEid = cEid; + + assertEq(cERC20Mock.balanceOf(userC), initialBalance); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), 0); + + vm.prank(userC); + vm.expectRevert( + abi.encodeWithSelector(IOFT.SlippageExceeded.selector, amountToSendLD, minAmountToCreditLD + 1) + ); + cOFTAdapter.debitView(amountToSendLD, minAmountToCreditLD + 1, dstEid); + + vm.prank(userC); + cERC20Mock.approve(address(cOFTAdapter), amountToSendLD); + vm.prank(userC); + (uint256 amountDebitedLD, uint256 amountToCreditLD) = cOFTAdapter.debit( + amountToSendLD, + minAmountToCreditLD, + dstEid + ); + + assertEq(amountDebitedLD, amountToSendLD); + assertEq(amountToCreditLD, amountToSendLD); + + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToSendLD); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + } + + function test_oft_adapter_debit_this() public { + uint256 amountToSendLD = 1 ether; + uint256 minAmountToCreditLD = 1 ether; + uint32 dstEid = cEid; + + assertEq(cERC20Mock.balanceOf(userC), initialBalance); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), 0); + + vm.prank(userC); + cERC20Mock.transfer(address(cOFTAdapter), amountToSendLD); + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToSendLD); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + // has no tokens to send + assertEq(cERC20Mock.balanceOf(userB), 0); + + // check outbound amount + assertEq(cOFTAdapter.outboundAmount(), 0); + + // reverts if a user tries to spend the tokens via debitThis AND the minimum exceeds the balance inside contract + vm.prank(userB); + vm.expectRevert( + abi.encodeWithSelector(IOFT.SlippageExceeded.selector, amountToSendLD, minAmountToCreditLD + 1) + ); + cOFTAdapter.debit(0, minAmountToCreditLD + 1, dstEid); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + + // Someone else can spend the tokens another user has sent into the contract + vm.prank(userB); + (uint256 amountDebitedLD, uint256 amountToCreditLD) = cOFTAdapter.debit(0, minAmountToCreditLD, dstEid); + + assertEq(amountDebitedLD, amountToSendLD); + assertEq(amountToCreditLD, amountToSendLD); + + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToSendLD); + assertEq(cERC20Mock.balanceOf(userB), 0); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + assertEq(cOFTAdapter.outboundAmount(), amountToSendLD); + } + + function test_oft_adapter_credit() public { + uint256 amountToCreditLD = 1 ether; + uint32 srcEid = cEid; + + assertEq(cERC20Mock.balanceOf(userC), initialBalance); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), 0); + + vm.prank(userC); + cERC20Mock.transfer(address(cOFTAdapter), amountToCreditLD); + cOFTAdapter.increaseOutboundAmount(amountToCreditLD); + + uint256 amountReceived = cOFTAdapter.credit(userB, amountToCreditLD, srcEid); + + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToCreditLD); + assertEq(cERC20Mock.balanceOf(address(userB)), amountReceived); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), 0); + assertEq(cOFTAdapter.outboundAmount(), 0); + } + + function test_oft_adapter_debit_this_dust_remains() public { + uint256 amountToSendLD = 1.23456789 ether; + uint256 minAmountToCreditLD = 1.234567 ether; + uint32 dstEid = aEid; + + assertEq(cERC20Mock.balanceOf(userC), initialBalance); + assertEq(cERC20Mock.balanceOf(address(this)), 0); + + vm.prank(userC); + cERC20Mock.transfer(address(cOFTAdapter), amountToSendLD); + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToSendLD); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + + assertEq(cOFTAdapter.outboundAmount(), 0); + + // Someone else can spend the tokens the user sent into the contract + vm.prank(userB); + (uint256 amountDebitedLD, uint256 amountToCreditLD) = cOFTAdapter.debit(0, minAmountToCreditLD, dstEid); + + assertEq(amountDebitedLD, cOFTAdapter.removeDust(amountToSendLD)); + assertEq(amountToCreditLD, amountDebitedLD); + assertEq(cOFTAdapter.outboundAmount(), amountDebitedLD); + + assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToSendLD); + assertEq(cERC20Mock.balanceOf(address(cOFTAdapter)), amountToSendLD); + } + + function decodeOFTMsgCodec( + bytes calldata message + ) public pure returns (bool isComposed, bytes32 sendTo, uint64 amountSD, bytes memory composeMsg) { + isComposed = OFTMsgCodec.isComposed(message); + sendTo = OFTMsgCodec.sendTo(message); + amountSD = OFTMsgCodec.amountSD(message); + composeMsg = OFTMsgCodec.composeMsg(message); + } + + function test_oft_build_msg() public { + uint32 dstEid = bEid; + bytes32 to = addressToBytes32(userA); + uint256 amountToSendLD = 1.23456789 ether; + uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); + + // params for buildMsgAndOptions + SendParam memory sendParam = SendParam(dstEid, to, amountToSendLD, minAmountToCreditLD); + bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory composeMsg = hex"1234"; + uint256 amountToCreditLD = minAmountToCreditLD; + + (bytes memory message, ) = aOFT.buildMsgAndOptions(sendParam, extraOptions, composeMsg, amountToCreditLD); + + (bool isComposed_, bytes32 sendTo_, uint64 amountSD_, bytes memory composeMsg_) = this.decodeOFTMsgCodec( + message + ); + + assertEq(isComposed_, true); + assertEq(sendTo_, to); + assertEq(amountSD_, aOFT.toSD(amountToCreditLD)); + bytes memory expectedComposeMsg = abi.encodePacked(addressToBytes32(address(this)), composeMsg); + assertEq(composeMsg_, expectedComposeMsg); + } + + function test_oft_build_msg_no_compose_msg() public { + uint32 dstEid = bEid; + bytes32 to = addressToBytes32(userA); + uint256 amountToSendLD = 1.23456789 ether; + uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); + + // params for buildMsgAndOptions + SendParam memory sendParam = SendParam(dstEid, to, amountToSendLD, minAmountToCreditLD); + bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory composeMsg = ""; + uint256 amountToCreditLD = minAmountToCreditLD; + + (bytes memory message, ) = aOFT.buildMsgAndOptions(sendParam, extraOptions, composeMsg, amountToCreditLD); + + (bool isComposed_, bytes32 sendTo_, uint64 amountSD_, bytes memory composeMsg_) = this.decodeOFTMsgCodec( + message + ); + + assertEq(isComposed_, false); + assertEq(sendTo_, to); + assertEq(amountSD_, aOFT.toSD(amountToCreditLD)); + assertEq(composeMsg_, ""); + } + + function test_set_enforced_options() public { + uint32 eid = 1; + + bytes memory optionsTypeOne = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory optionsTypeTwo = OptionsBuilder.newOptions().addExecutorLzReceiveOption(250000, 0); + + EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2); + enforcedOptions[0] = EnforcedOptionParam(eid, 1, optionsTypeOne); + enforcedOptions[1] = EnforcedOptionParam(eid, 2, optionsTypeTwo); + + aOFT.setEnforcedOptions(enforcedOptions); + + assertEq(aOFT.enforcedOptions(eid, 1), optionsTypeOne); + assertEq(aOFT.enforcedOptions(eid, 2), optionsTypeTwo); + } + + function test_assert_options_type3_revert() public { + uint32 eid = 1; + EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](1); + + enforcedOptions[0] = EnforcedOptionParam(eid, 1, hex"0004"); // not type 3 + vm.expectRevert(abi.encodeWithSelector(IOAppOptionsType3.InvalidOptions.selector, hex"0004")); + aOFT.setEnforcedOptions(enforcedOptions); + + enforcedOptions[0] = EnforcedOptionParam(eid, 1, hex"0002"); // not type 3 + vm.expectRevert(abi.encodeWithSelector(IOAppOptionsType3.InvalidOptions.selector, hex"0002")); + aOFT.setEnforcedOptions(enforcedOptions); + + enforcedOptions[0] = EnforcedOptionParam(eid, 1, hex"0001"); // not type 3 + vm.expectRevert(abi.encodeWithSelector(IOAppOptionsType3.InvalidOptions.selector, hex"0001")); + aOFT.setEnforcedOptions(enforcedOptions); + + enforcedOptions[0] = EnforcedOptionParam(eid, 1, hex"0003"); // not type 3 + aOFT.setEnforcedOptions(enforcedOptions); // doesnt revert cus option type 3 + } + + function test_combine_options() public { + uint32 eid = 1; + uint16 msgType = 1; + + bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + EnforcedOptionParam[] memory enforcedOptionsArray = new EnforcedOptionParam[](1); + enforcedOptionsArray[0] = EnforcedOptionParam(eid, msgType, enforcedOptions); + aOFT.setEnforcedOptions(enforcedOptionsArray); + + bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( + 1.2345 ether, + addressToBytes32(userA) + ); + + bytes memory expectedOptions = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorNativeDropOption(1.2345 ether, addressToBytes32(userA)); + + bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, extraOptions); + assertEq(combinedOptions, expectedOptions); + } + + function test_combine_options_no_extra_options() public { + uint32 eid = 1; + uint16 msgType = 1; + + bytes memory enforcedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + EnforcedOptionParam[] memory enforcedOptionsArray = new EnforcedOptionParam[](1); + enforcedOptionsArray[0] = EnforcedOptionParam(eid, msgType, enforcedOptions); + aOFT.setEnforcedOptions(enforcedOptionsArray); + + bytes memory expectedOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + + bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, ""); + assertEq(combinedOptions, expectedOptions); + } + + function test_combine_options_no_enforced_options() public { + uint32 eid = 1; + uint16 msgType = 1; + + bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( + 1.2345 ether, + addressToBytes32(userA) + ); + + bytes memory expectedOptions = OptionsBuilder.newOptions().addExecutorNativeDropOption( + 1.2345 ether, + addressToBytes32(userA) + ); + + bytes memory combinedOptions = aOFT.combineOptions(eid, msgType, extraOptions); + assertEq(combinedOptions, expectedOptions); + } + + function test_oapp_inspector_inspect() public { + uint32 dstEid = bEid; + bytes32 to = addressToBytes32(userA); + uint256 amountToSendLD = 1.23456789 ether; + uint256 minAmountToCreditLD = aOFT.removeDust(amountToSendLD); + + // params for buildMsgAndOptions + SendParam memory sendParam = SendParam(dstEid, to, amountToSendLD, minAmountToCreditLD); + bytes memory extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + bytes memory composeMsg = ""; + uint256 amountToCreditLD = minAmountToCreditLD; + + // doesnt revert + (bytes memory message, ) = aOFT.buildMsgAndOptions(sendParam, extraOptions, composeMsg, amountToCreditLD); + + // deploy a universal inspector, it automatically reverts + oAppInspector = new OFTInspectorMock(); + // set the inspector + aOFT.setMsgInspector(address(oAppInspector)); + + // does revert because inspector is set + vm.expectRevert(abi.encodeWithSelector(IOAppMsgInspector.InspectionFailed.selector, message, extraOptions)); + (message, ) = aOFT.buildMsgAndOptions(sendParam, extraOptions, composeMsg, amountToCreditLD); + } +} diff --git a/oapp/test/OmniCounter.t.sol b/oapp/test/OmniCounter.t.sol new file mode 100644 index 0000000..12690ac --- /dev/null +++ b/oapp/test/OmniCounter.t.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.15; + +import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { Errors } from "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/Errors.sol"; + +import { OptionsBuilder } from "../contracts/oapp/libs/OptionsBuilder.sol"; +import { OmniCounter, MsgCodec } from "../contracts/oapp/examples/OmniCounter.sol"; +import { OmniCounterPreCrime } from "../contracts/oapp/examples/OmniCounterPreCrime.sol"; +import { PreCrimePeer } from "../contracts/precrime/interfaces/IPreCrime.sol"; + +import { TestHelper } from "./TestHelper.sol"; + +import "forge-std/console.sol"; + +contract OmniCounterTest is TestHelper { + using OptionsBuilder for bytes; + + uint32 aEid = 1; + uint32 bEid = 2; + + // omnicounter with precrime + OmniCounter aCounter; + OmniCounterPreCrime aPreCrime; + OmniCounter bCounter; + OmniCounterPreCrime bPreCrime; + + address offchain = address(0xDEAD); + + error CrimeFound(bytes crime); + + function setUp() public virtual override { + super.setUp(); + + setUpEndpoints(2, LibraryType.UltraLightNode); + + address[] memory uas = setupOApps(type(OmniCounter).creationCode, 1, 2); + aCounter = OmniCounter(payable(uas[0])); + bCounter = OmniCounter(payable(uas[1])); + + setUpPreCrime(); + } + + function setUpPreCrime() public { + // set up precrime for aCounter + aPreCrime = new OmniCounterPreCrime(address(aCounter.endpoint()), address(aCounter), address(this)); + aPreCrime.setMaxBatchSize(10); + + PreCrimePeer[] memory aCounterPreCrimePeers = new PreCrimePeer[](1); + aCounterPreCrimePeers[0] = PreCrimePeer( + bEid, + addressToBytes32(address(bPreCrime)), + addressToBytes32(address(bCounter)) + ); + aPreCrime.setPreCrimePeers(aCounterPreCrimePeers); + + aCounter.setPreCrime(address(aPreCrime)); + + // set up precrime for bCounter + bPreCrime = new OmniCounterPreCrime(address(bCounter.endpoint()), address(bCounter), address(this)); + bPreCrime.setMaxBatchSize(10); + + PreCrimePeer[] memory bCounterPreCrimePeers = new PreCrimePeer[](1); + bCounterPreCrimePeers[0] = PreCrimePeer( + aEid, + addressToBytes32(address(aPreCrime)), + addressToBytes32(address(aCounter)) + ); + bPreCrime.setPreCrimePeers(bCounterPreCrimePeers); + + bCounter.setPreCrime(address(bPreCrime)); + } + + // classic message passing A -> B + function test_increment() public { + uint256 counterBefore = bCounter.count(); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + + assertEq(bCounter.count(), counterBefore, "shouldn't be increased until packet is verified"); + + // verify packet to bCounter manually + verifyPackets(bEid, addressToBytes32(address(bCounter))); + + assertEq(bCounter.count(), counterBefore + 1, "increment assertion failure"); + } + + function test_batchIncrement() public { + uint256 counterBefore = bCounter.count(); + + uint256 batchSize = 5; + uint32[] memory eids = new uint32[](batchSize); + uint8[] memory types = new uint8[](batchSize); + bytes[] memory options = new bytes[](batchSize); + bytes memory option = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + uint256 fee; + for (uint256 i = 0; i < batchSize; i++) { + eids[i] = bEid; + types[i] = MsgCodec.VANILLA_TYPE; + options[i] = option; + (uint256 nativeFee, ) = aCounter.quote(eids[i], types[i], options[i]); + fee += nativeFee; + } + + vm.expectRevert(); // Errors.InvalidAmount + aCounter.batchIncrement{ value: fee - 1 }(eids, types, options); + + aCounter.batchIncrement{ value: fee }(eids, types, options); + verifyPackets(bEid, addressToBytes32(address(bCounter))); + + assertEq(bCounter.count(), counterBefore + batchSize, "batchIncrement assertion failure"); + } + + function test_nativeDrop_increment() public { + uint256 balanceBefore = address(bCounter).balance; + + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorNativeDropOption(1 gwei, addressToBytes32(address(bCounter))); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + + // verify packet to bCounter manually + verifyPackets(bEid, addressToBytes32(address(bCounter))); + + assertEq(address(bCounter).balance, balanceBefore + 1 gwei, "nativeDrop assertion failure"); + + // transfer funds out + address payable receiver = payable(address(0xABCD)); + + // withdraw with non admin + vm.startPrank(receiver); + vm.expectRevert(); + bCounter.withdraw(receiver, 1 gwei); + vm.stopPrank(); + + // withdraw with admin + bCounter.withdraw(receiver, 1 gwei); + assertEq(address(bCounter).balance, 0, "withdraw assertion failure"); + assertEq(receiver.balance, 1 gwei, "withdraw assertion failure"); + } + + // classic message passing A -> B1 -> B2 + function test_lzCompose_increment() public { + uint256 countBefore = bCounter.count(); + uint256 composedCountBefore = bCounter.composedCount(); + + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorLzComposeOption(0, 200000, 0); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.COMPOSED_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.COMPOSED_TYPE, options); + + verifyPackets(bEid, addressToBytes32(address(bCounter)), 0, address(bCounter)); + + assertEq(bCounter.count(), countBefore + 1, "increment B1 assertion failure"); + assertEq(bCounter.composedCount(), composedCountBefore + 1, "increment B2 assertion failure"); + } + + // A -> B -> A + function test_ABA_increment() public { + uint256 countABefore = aCounter.count(); + uint256 countBBefore = bCounter.count(); + + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(10000000, 10000000); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.ABA_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.ABA_TYPE, options); + + verifyPackets(bEid, addressToBytes32(address(bCounter))); + assertEq(aCounter.count(), countABefore, "increment A assertion failure"); + assertEq(bCounter.count(), countBBefore + 1, "increment B assertion failure"); + + verifyPackets(aEid, addressToBytes32(address(aCounter))); + assertEq(aCounter.count(), countABefore + 1, "increment A assertion failure"); + } + + // A -> B1 -> B2 -> A + function test_lzCompose_ABA_increment() public { + uint256 countABefore = aCounter.count(); + uint256 countBBefore = bCounter.count(); + uint256 composedCountBBefore = bCounter.composedCount(); + + bytes memory options = OptionsBuilder + .newOptions() + .addExecutorLzReceiveOption(200000, 0) + .addExecutorLzComposeOption(0, 10000000, 10000000); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.COMPOSED_ABA_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.COMPOSED_ABA_TYPE, options); + + verifyPackets(bEid, addressToBytes32(address(bCounter)), 0, address(bCounter)); + assertEq(bCounter.count(), countBBefore + 1, "increment B1 assertion failure"); + assertEq(bCounter.composedCount(), composedCountBBefore + 1, "increment B2 assertion failure"); + + verifyPackets(aEid, addressToBytes32(address(aCounter))); + assertEq(aCounter.count(), countABefore + 1, "increment A assertion failure"); + } + + function test_omniCounterPreCrime() public { + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0); + (uint256 nativeFee, ) = aCounter.quote(bEid, MsgCodec.VANILLA_TYPE, options); + + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + aCounter.increment{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + assertEq(aCounter.outboundCount(bEid), 2, "outboundCount assertion failure"); + + // precrime should pass + bytes[] memory packets = new bytes[](2); + uint256[] memory packetMsgValues = new uint256[](2); + bytes memory message = MsgCodec.encode(MsgCodec.VANILLA_TYPE, aEid); + packets[0] = PacketV1Codec.encode( + Packet(1, aEid, address(aCounter), bEid, addressToBytes32(address(bCounter)), bytes32(0), message) + ); + packets[1] = PacketV1Codec.encode( + Packet(2, aEid, address(aCounter), bEid, addressToBytes32(address(bCounter)), bytes32(0), message) + ); + + vm.startPrank(offchain); + + bytes[] memory simulations = new bytes[](2); + simulations[0] = aPreCrime.simulate(new bytes[](0), new uint256[](0)); + simulations[1] = bPreCrime.simulate(packets, packetMsgValues); + + bPreCrime.preCrime(packets, packetMsgValues, simulations); + + verifyPackets(bEid, addressToBytes32(address(bCounter))); + assertEq(bCounter.inboundCount(aEid), 2, "inboundCount assertion failure"); + + vm.startPrank(address(this)); + + // precrime a broken increment + aCounter.brokenIncrement{ value: nativeFee }(bEid, MsgCodec.VANILLA_TYPE, options); + assertEq(aCounter.outboundCount(bEid), 2, "outboundCount assertion failure"); // broken outbound increment + + packets = new bytes[](1); + packetMsgValues = new uint256[](1); + packets[0] = PacketV1Codec.encode( + Packet(3, aEid, address(aCounter), bEid, addressToBytes32(address(bCounter)), bytes32(0), message) + ); + + vm.startPrank(offchain); + + simulations[0] = aPreCrime.simulate(new bytes[](0), new uint256[](0)); + simulations[1] = bPreCrime.simulate(packets, packetMsgValues); + + bytes memory expectedError = abi.encodeWithSelector(CrimeFound.selector, "inboundCount > outboundCount"); + vm.expectRevert(expectedError); + + bPreCrime.preCrime(packets, packetMsgValues, simulations); + + verifyPackets(bEid, addressToBytes32(address(bCounter))); + assertEq(bCounter.inboundCount(aEid), 3, "inboundCount assertion failure"); // state broken + } +} diff --git a/oapp/test/OptionsHelper.sol b/oapp/test/OptionsHelper.sol new file mode 100644 index 0000000..bf227cd --- /dev/null +++ b/oapp/test/OptionsHelper.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.0; + +import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import { UlnOptions } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/libs/UlnOptions.sol"; + +contract UlnOptionsMock { + using UlnOptions for bytes; + + function decode( + bytes calldata _options + ) public pure returns (bytes memory executorOptions, bytes memory dvnOptions) { + return UlnOptions.decode(_options); + } +} + +contract OptionsHelper { + UlnOptionsMock ulnOptions = new UlnOptionsMock(); + + function _parseExecutorLzReceiveOption(bytes memory _options) internal view returns (uint256 gas, uint256 value) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_LZRECEIVE + ); + require(exist, "OptionsHelper: lzReceive option not found"); + (gas, value) = this.decodeLzReceiveOption(option); + } + + function _parseExecutorNativeDropOption( + bytes memory _options + ) internal view returns (uint256 amount, bytes32 receiver) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_NATIVE_DROP + ); + require(exist, "OptionsHelper: nativeDrop option not found"); + (amount, receiver) = this.decodeNativeDropOption(option); + } + + function _parseExecutorLzComposeOption( + bytes memory _options + ) internal view returns (uint16 index, uint256 gas, uint256 value) { + (bool exist, bytes memory option) = _getExecutorOptionByOptionType( + _options, + ExecutorOptions.OPTION_TYPE_LZCOMPOSE + ); + require(exist, "OptionsHelper: lzCompose option not found"); + return this.decodeLzComposeOption(option); + } + + function _executorOptionExists( + bytes memory _options, + uint8 _executorOptionType + ) internal view returns (bool exist) { + (exist, ) = _getExecutorOptionByOptionType(_options, _executorOptionType); + } + + function _getExecutorOptionByOptionType( + bytes memory _options, + uint8 _executorOptionType + ) internal view returns (bool exist, bytes memory option) { + (bytes memory executorOpts, ) = ulnOptions.decode(_options); + + uint256 cursor; + while (cursor < executorOpts.length) { + (uint8 optionType, bytes memory op, uint256 nextCursor) = this.nextExecutorOption(executorOpts, cursor); + if (optionType == _executorOptionType) { + return (true, op); + } + cursor = nextCursor; + } + } + + function nextExecutorOption( + bytes calldata _options, + uint256 _cursor + ) external pure returns (uint8 optionType, bytes calldata option, uint256 cursor) { + return ExecutorOptions.nextExecutorOption(_options, _cursor); + } + + function decodeLzReceiveOption(bytes calldata _option) external pure returns (uint128 gas, uint128 value) { + return ExecutorOptions.decodeLzReceiveOption(_option); + } + + function decodeNativeDropOption(bytes calldata _option) external pure returns (uint128 amount, bytes32 receiver) { + return ExecutorOptions.decodeNativeDropOption(_option); + } + + function decodeLzComposeOption( + bytes calldata _option + ) external pure returns (uint16 index, uint128 gas, uint128 value) { + return ExecutorOptions.decodeLzComposeOption(_option); + } +} diff --git a/oapp/test/PreCrimeV2.t.sol b/oapp/test/PreCrimeV2.t.sol new file mode 100644 index 0000000..8c43dbc --- /dev/null +++ b/oapp/test/PreCrimeV2.t.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.15; + +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { IPreCrime, PreCrimePeer } from "../contracts/precrime/interfaces/IPreCrime.sol"; +import { InboundPacket } from "../contracts/precrime/libs/Packet.sol"; + +import { TestHelper } from "./TestHelper.sol"; +import { PreCrimeV2Mock } from "./mocks/PreCrimeV2Mock.sol"; +import { PreCrimeV2SimulatorMock } from "./mocks/PreCrimeV2SimulatorMock.sol"; + +import "forge-std/console.sol"; + +contract PreCrimeV2Test is TestHelper { + uint16 constant CONFIG_VERSION = 2; + uint64 constant MAX_BATCH_SIZE = 4; + address constant OFF_CHAIN = address(0xDEAD); + + PreCrimeV2Mock preCrime; + PreCrimeV2SimulatorMock simulator; + + PreCrimePeer[] preCrimePeers; + + function setUp() public override { + super.setUp(); + + setUpEndpoints(1, LibraryType.SimpleMessageLib); + + simulator = new PreCrimeV2SimulatorMock(); + preCrime = new PreCrimeV2Mock(address(endpoints[1]), address(simulator)); + + preCrimePeers.push(PreCrimePeer(2, bytes32(uint256(22)), bytes32(uint256(2)))); + preCrimePeers.push(PreCrimePeer(3, bytes32(uint256(33)), bytes32(uint256(3)))); + + preCrime.setPreCrimePeers(preCrimePeers); + preCrime.setMaxBatchSize(MAX_BATCH_SIZE); + } + + function test_getConfig() public { + uint256[] memory packetMsgValues = new uint256[](1); + + // return config with all peers if no packet + vm.startPrank(OFF_CHAIN); + bytes memory config = preCrime.getConfig(new bytes[](0), packetMsgValues); + bytes memory expectedConfig = abi.encodePacked(CONFIG_VERSION, MAX_BATCH_SIZE, _encodePeers(preCrimePeers)); + assertEq(config, expectedConfig); + + // return config without peers if packet from untrusted peer + bytes[] memory packets = _buildPacket(2, bytes32(0), 1, 1); // untrusted peer + config = preCrime.getConfig(packets, packetMsgValues); + expectedConfig = abi.encodePacked(CONFIG_VERSION, MAX_BATCH_SIZE); + assertEq(config, expectedConfig); + + // return config with peers if packet from trusted peer + packets = _buildPacket(2, bytes32(uint256(2)), 1, 1); // trusted peer + config = preCrime.getConfig(packets, packetMsgValues); + expectedConfig = abi.encodePacked(CONFIG_VERSION, MAX_BATCH_SIZE, _encodePeers(preCrimePeers)); + assertEq(config, expectedConfig); + } + + function test_simulate_packetOverSize() public { + uint256[] memory packetMsgValues = new uint256[](5); + bytes[] memory packets = _buildPacket(2, bytes32(uint256(2)), 1, 5); // too many packets + vm.startPrank(OFF_CHAIN); + bytes memory expectedError = abi.encodeWithSelector(IPreCrime.PacketOversize.selector, 4, 5); + vm.expectRevert(expectedError); + preCrime.simulate(packets, packetMsgValues); + } + + function test_simulate_packetUnsorted() public { + uint256[] memory packetMsgValues = new uint256[](4); + bytes[] memory unsortedPackets = new bytes[](4); + unsortedPackets[0] = _buildPacket(2, bytes32(uint256(2)), 1, 1)[0]; + unsortedPackets[1] = _buildPacket(3, bytes32(uint256(3)), 1, 1)[0]; // unsorted + unsortedPackets[2] = _buildPacket(2, bytes32(uint256(2)), 2, 1)[0]; + unsortedPackets[3] = _buildPacket(3, bytes32(uint256(4)), 1, 1)[0]; // untrested peer, but skipped + + vm.startPrank(OFF_CHAIN); + bytes memory expectedError = abi.encodeWithSelector(IPreCrime.PacketUnsorted.selector); + vm.expectRevert(expectedError); + preCrime.simulate(unsortedPackets, packetMsgValues); + } + + function test_simulate_failed() public { + // empty packetMsgValues to be reused + uint256[] memory packetMsgValues = new uint256[](1); + bytes[] memory packets = _buildPacket(0, bytes32(0), 1, 1); // invalid packet and simulation failed + vm.startPrank(OFF_CHAIN); + bytes memory expectedError = abi.encodeWithSelector( + IPreCrime.SimulationFailed.selector, + abi.encodeWithSelector(PreCrimeV2SimulatorMock.InvalidEid.selector) + ); + vm.expectRevert(expectedError); + preCrime.simulate(packets, packetMsgValues); + } + + function test_simulate() public { + uint256[] memory packetMsgValues = new uint256[](4); + bytes[] memory packets = _buildPacket(2, bytes32(uint256(2)), 1, 2); + packets = _appendPackets(packets, _buildPacket(3, bytes32(uint256(3)), 1, 2)); + + vm.startPrank(OFF_CHAIN); + bytes memory result = preCrime.simulate(packets, packetMsgValues); + bytes memory expectedResult = abi.encodePacked(uint32(1), uint256(4)); // receive 4 packets + assertEq(result, expectedResult); + } + + function test_preCrime_simulationResultNotFound() public { + uint256[] memory packetMsgValues = new uint256[](1); + bytes[] memory packets = _buildPacket(2, bytes32(uint256(2)), 1, 1); + + // result of eid 3 not found + bytes[] memory results = new bytes[](2); + results[0] = abi.encodePacked(uint32(1), uint256(1)); + results[1] = abi.encodePacked(uint32(2), uint256(1)); + + vm.startPrank(OFF_CHAIN); + bytes memory expectedError = abi.encodeWithSelector(IPreCrime.SimulationResultNotFound.selector, 3); + vm.expectRevert(expectedError); + preCrime.preCrime(packets, packetMsgValues, results); + + // result of eid 1 (local result) not found + results[0] = abi.encodePacked(uint32(2), uint256(1)); + results[1] = abi.encodePacked(uint32(3), uint256(1)); + + expectedError = abi.encodeWithSelector(IPreCrime.SimulationResultNotFound.selector, 1); + vm.expectRevert(expectedError); + preCrime.preCrime(packets, packetMsgValues, results); + } + + function test_preCrime() public { + uint256[] memory packetMsgValues = new uint256[](1); + bytes[] memory packets = _buildPacket(2, bytes32(uint256(2)), 1, 1); + + bytes[] memory results = new bytes[](3); + results[0] = abi.encodePacked(uint32(1), uint256(1)); + results[1] = abi.encodePacked(uint32(2), uint256(2)); + results[2] = abi.encodePacked(uint32(3), uint256(3)); + + vm.startPrank(OFF_CHAIN); + preCrime.preCrime(packets, packetMsgValues, results); + + // check internal state of preCrime + assertEq(preCrime.eids(0), 1); + assertEq(preCrime.eids(1), 2); + assertEq(preCrime.eids(2), 3); + assertEq(preCrime.results(0), abi.encode(1)); + assertEq(preCrime.results(1), abi.encode(2)); + assertEq(preCrime.results(2), abi.encode(3)); + } + + function _buildPacket( + uint32 _srcEid, + bytes32 _sender, + uint64 _nonce, + uint256 _packetNum + ) internal view returns (bytes[] memory) { + bytes[] memory packets = new bytes[](_packetNum); + for (uint256 i = 0; i < _packetNum; ++i) { + InboundPacket memory packet = InboundPacket( + Origin(_srcEid, _sender, _nonce + uint64(i)), + 1, + preCrime.oApp(), + bytes32(0), + 0, + address(0), + "", + "" + ); + packets[i] = _encodePacket(packet); + } + return packets; + } + + function _encodePacket(InboundPacket memory _packet) internal pure returns (bytes memory encodedPacket) { + encodedPacket = abi.encodePacked( + uint8(1), + _packet.origin.nonce, + _packet.origin.srcEid, + _packet.origin.sender, + _packet.dstEid, + bytes32(uint256(uint160(_packet.receiver))), + _packet.guid, + _packet.value, + _packet.message + ); + } + + function _appendPackets( + bytes[] memory _packets, + bytes[] memory _newPackets + ) internal pure returns (bytes[] memory) { + bytes[] memory packets = new bytes[](_packets.length + _newPackets.length); + for (uint256 i = 0; i < _packets.length; ++i) { + packets[i] = _packets[i]; + } + for (uint256 i = 0; i < _newPackets.length; ++i) { + packets[_packets.length + i] = _newPackets[i]; + } + return packets; + } + + function _encodePeers(PreCrimePeer[] memory _peers) internal pure returns (bytes memory) { + bytes memory peers = abi.encodePacked(uint16(_peers.length)); + for (uint256 i = 0; i < _peers.length; ++i) { + peers = abi.encodePacked(peers, _peers[i].eid, _peers[i].preCrime, _peers[i].oApp); + } + return peers; + } +} diff --git a/oapp/test/TestHelper.sol b/oapp/test/TestHelper.sol new file mode 100644 index 0000000..6f370a9 --- /dev/null +++ b/oapp/test/TestHelper.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.18; + +import { Test } from "forge-std/Test.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; + +import { UlnConfig, SetDefaultUlnConfigParam } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/UlnBase.sol"; +import { SetDefaultExecutorConfigParam, ExecutorConfig } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/SendLibBase.sol"; +import { ReceiveUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/ReceiveUln302.sol"; +import { DVN, ExecuteParam } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/dvn/DVN.sol"; +import { DVNFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/dvn/DVNFeeLib.sol"; +import { IExecutor } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/IExecutor.sol"; +import { Executor } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/Executor.sol"; +import { PriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/PriceFeed.sol"; +import { ILayerZeroPriceFeed } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/interfaces/ILayerZeroPriceFeed.sol"; +import { IReceiveUlnE2 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/interfaces/IReceiveUlnE2.sol"; +import { ReceiveUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/ReceiveUln302.sol"; +import { IMessageLib } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/IMessageLib.sol"; +import { EndpointV2 } from "@layerzerolabs/lz-evm-protocol-v2/contracts/EndpointV2.sol"; +import { ExecutorOptions } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/ExecutorOptions.sol"; +import { PacketV1Codec } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/libs/PacketV1Codec.sol"; +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { OApp } from "../contracts/oapp/OApp.sol"; +import { OptionsBuilder } from "../contracts/oapp/libs/OptionsBuilder.sol"; + +import { OptionsHelper } from "./OptionsHelper.sol"; +import { SendUln302Mock as SendUln302 } from "./mocks/SendUln302Mock.sol"; +import { SimpleMessageLibMock } from "./mocks/SimpleMessageLibMock.sol"; +import "./mocks/ExecutorFeeLibMock.sol"; + +import "forge-std/console.sol"; + +contract TestHelper is Test, OptionsHelper { + using OptionsBuilder for bytes; + + enum LibraryType { + UltraLightNode, + SimpleMessageLib + } + + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + using PacketV1Codec for bytes; + + mapping(uint32 => mapping(bytes32 => DoubleEndedQueue.Bytes32Deque)) packetsQueue; // dstEid => dstUA => guids queue + mapping(bytes32 => bytes) packets; // guid => packet bytes + mapping(bytes32 => bytes) optionsLookup; // guid => options + + mapping(uint32 => address) endpoints; // eid => endpoint + + uint256 public constant TREASURY_GAS_CAP = 1000000000000; + uint256 public constant TREASURY_GAS_FOR_FEE_CAP = 100000; + + function setUp() public virtual {} + + /** + * @dev setup the endpoints + * @param _endpointNum num of endpoints + */ + function setUpEndpoints(uint8 _endpointNum, LibraryType _libraryType) public { + EndpointV2[] memory endpointList = new EndpointV2[](_endpointNum); + uint32[] memory eidList = new uint32[](_endpointNum); + + // deploy _excludedContracts + for (uint8 i = 0; i < _endpointNum; i++) { + uint32 eid = i + 1; + eidList[i] = eid; + endpointList[i] = new EndpointV2(eid, address(this)); + registerEndpoint(endpointList[i]); + } + + // deploy + address[] memory sendLibs = new address[](_endpointNum); + address[] memory receiveLibs = new address[](_endpointNum); + + address[] memory signers = new address[](1); + signers[0] = vm.addr(1); + + PriceFeed priceFeed = new PriceFeed(); + priceFeed.initialize(address(this)); + + for (uint8 i = 0; i < _endpointNum; i++) { + if (_libraryType == LibraryType.UltraLightNode) { + address endpointAddr = address(endpointList[i]); + + SendUln302 sendUln; + ReceiveUln302 receiveUln; + { + sendUln = new SendUln302(payable(this), endpointAddr, TREASURY_GAS_CAP, TREASURY_GAS_FOR_FEE_CAP); + receiveUln = new ReceiveUln302(endpointAddr); + endpointList[i].registerLibrary(address(sendUln)); + endpointList[i].registerLibrary(address(receiveUln)); + sendLibs[i] = address(sendUln); + receiveLibs[i] = address(receiveUln); + } + + Executor executor = new Executor(); + DVN dvn; + { + address[] memory admins = new address[](1); + admins[0] = address(this); + + address[] memory messageLibs = new address[](2); + messageLibs[0] = address(sendUln); + messageLibs[1] = address(receiveUln); + + executor.initialize( + endpointAddr, + address(0x0), + messageLibs, + address(priceFeed), + address(this), + admins + ); + ExecutorFeeLib executorLib = new ExecutorFeeLibMock(); + executor.setWorkerFeeLib(address(executorLib)); + + dvn = new DVN(i + 1, messageLibs, address(priceFeed), signers, 1, admins); + DVNFeeLib dvnLib = new DVNFeeLib(1e18); + dvn.setWorkerFeeLib(address(dvnLib)); + } + + //todo: setDstGas + uint32 endpointNum = _endpointNum; + IExecutor.DstConfigParam[] memory dstConfigParams = new IExecutor.DstConfigParam[](endpointNum); + for (uint8 j = 0; j < endpointNum; j++) { + if (i == j) continue; + uint32 dstEid = j + 1; + + address[] memory defaultDVNs = new address[](1); + address[] memory optionalDVNs = new address[](0); + defaultDVNs[0] = address(dvn); + + { + SetDefaultUlnConfigParam[] memory params = new SetDefaultUlnConfigParam[](1); + UlnConfig memory ulnConfig = UlnConfig( + 100, + uint8(defaultDVNs.length), + uint8(optionalDVNs.length), + 0, + defaultDVNs, + optionalDVNs + ); + params[0] = SetDefaultUlnConfigParam(dstEid, ulnConfig); + sendUln.setDefaultUlnConfigs(params); + } + + { + SetDefaultExecutorConfigParam[] memory params = new SetDefaultExecutorConfigParam[](1); + ExecutorConfig memory executorConfig = ExecutorConfig(10000, address(executor)); + params[0] = SetDefaultExecutorConfigParam(dstEid, executorConfig); + sendUln.setDefaultExecutorConfigs(params); + } + + { + SetDefaultUlnConfigParam[] memory params = new SetDefaultUlnConfigParam[](1); + UlnConfig memory ulnConfig = UlnConfig( + 100, + uint8(defaultDVNs.length), + uint8(optionalDVNs.length), + 0, + defaultDVNs, + optionalDVNs + ); + params[0] = SetDefaultUlnConfigParam(dstEid, ulnConfig); + receiveUln.setDefaultUlnConfigs(params); + } + + // executor config + dstConfigParams[j] = IExecutor.DstConfigParam({ + dstEid: dstEid, + baseGas: 5000, + multiplierBps: 10000, + floorMarginUSD: 1e10, + nativeCap: 1 gwei + }); + + uint128 denominator = priceFeed.getPriceRatioDenominator(); + ILayerZeroPriceFeed.UpdatePrice[] memory prices = new ILayerZeroPriceFeed.UpdatePrice[](1); + prices[0] = ILayerZeroPriceFeed.UpdatePrice( + dstEid, + ILayerZeroPriceFeed.Price(1 * denominator, 1, 1) + ); + priceFeed.setPrice(prices); + } + executor.setDstConfig(dstConfigParams); + } else if (_libraryType == LibraryType.SimpleMessageLib) { + SimpleMessageLibMock messageLib = new SimpleMessageLibMock(payable(this), address(endpointList[i])); + endpointList[i].registerLibrary(address(messageLib)); + sendLibs[i] = address(messageLib); + receiveLibs[i] = address(messageLib); + } else { + revert("invalid library type"); + } + } + + // config up + for (uint8 i = 0; i < _endpointNum; i++) { + EndpointV2 endpoint = endpointList[i]; + for (uint8 j = 0; j < _endpointNum; j++) { + if (i == j) continue; + endpoint.setDefaultSendLibrary(j + 1, sendLibs[i]); + endpoint.setDefaultReceiveLibrary(j + 1, receiveLibs[i], 0); + } + } + } + + /** + * @dev setup UAs, only if the UA has `endpoint` address as the unique parameter + */ + function setupOApps( + bytes memory _oappCreationCode, + uint8 _startEid, + uint8 _oappNum + ) public returns (address[] memory oapps) { + oapps = new address[](_oappNum); + for (uint8 eid = _startEid; eid < _startEid + _oappNum; eid++) { + address oapp = _deployOApp(_oappCreationCode, abi.encode(address(endpoints[eid]), address(this), true)); + oapps[eid - _startEid] = oapp; + } + // config + wireOApps(oapps); + } + + function wireOApps(address[] memory oapps) public { + uint256 size = oapps.length; + for (uint256 i = 0; i < size; i++) { + OApp localOApp = OApp(payable(oapps[i])); + for (uint256 j = 0; j < size; j++) { + if (i == j) continue; + OApp remoteOApp = OApp(payable(oapps[j])); + uint32 remoteEid = (remoteOApp.endpoint()).eid(); + localOApp.setPeer(remoteEid, addressToBytes32(address(remoteOApp))); + } + } + } + + function _deployOApp(bytes memory _oappBytecode, bytes memory _constructorArgs) internal returns (address addr) { + bytes memory bytecode = bytes.concat(abi.encodePacked(_oappBytecode), _constructorArgs); + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + } + + function schedulePacket(bytes calldata _packetBytes, bytes calldata _options) public { + uint32 dstEid = _packetBytes.dstEid(); + bytes32 dstAddress = _packetBytes.receiver(); + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[dstEid][dstAddress]; + // front in, back out + bytes32 guid = _packetBytes.guid(); + queue.pushFront(guid); + packets[guid] = _packetBytes; + optionsLookup[guid] = _options; + } + + /** + * @dev verify packets to destination chain's UA address + * @param _dstEid destination eid + * @param _dstAddress destination address + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress) public { + verifyPackets(_dstEid, _dstAddress, 0, address(0x0)); + } + + /** + * @dev verify packets to destination chain's UA address + * @param _dstEid destination eid + * @param _dstAddress destination address + */ + function verifyPackets(uint32 _dstEid, address _dstAddress) public { + verifyPackets(_dstEid, bytes32(uint256(uint160(_dstAddress))), 0, address(0x0)); + } + + /** + * @dev dst UA receive/execute packets + * @dev will NOT work calling this directly with composer IF the composed payload is different from the lzReceive msg payload + */ + function verifyPackets(uint32 _dstEid, bytes32 _dstAddress, uint256 _packetAmount, address _composer) public { + require(endpoints[_dstEid] != address(0), "endpoint not yet registered"); + + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + uint256 pendingPacketsSize = queue.length(); + uint256 numberOfPackets; + if (_packetAmount == 0) { + numberOfPackets = queue.length(); + } else { + numberOfPackets = pendingPacketsSize > _packetAmount ? _packetAmount : pendingPacketsSize; + } + while (numberOfPackets > 0) { + numberOfPackets--; + // front in, back out + bytes32 guid = queue.popBack(); + bytes memory packetBytes = packets[guid]; + this.assertGuid(packetBytes, guid); + this.validatePacket(packetBytes); + + bytes memory options = optionsLookup[guid]; + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_NATIVE_DROP)) { + (uint256 amount, bytes32 receiver) = _parseExecutorNativeDropOption(options); + address to = address(uint160(uint256(receiver))); + (bool sent, ) = to.call{ value: amount }(""); + require(sent, "Failed to send Ether"); + } + if (_executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZRECEIVE)) { + this.lzReceive(packetBytes, options); + } + if (_composer != address(0) && _executorOptionExists(options, ExecutorOptions.OPTION_TYPE_LZCOMPOSE)) { + this.lzCompose(packetBytes, options, guid, _composer); + } + } + } + + function lzReceive(bytes calldata _packetBytes, bytes memory _options) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_packetBytes.dstEid()]); + (uint256 gas, uint256 value) = OptionsHelper._parseExecutorLzReceiveOption(_options); + + Origin memory origin = Origin(_packetBytes.srcEid(), _packetBytes.sender(), _packetBytes.nonce()); + endpoint.lzReceive{ value: value, gas: gas }( + origin, + _packetBytes.receiverB20(), + _packetBytes.guid(), + _packetBytes.message(), + bytes("") + ); + } + + function lzCompose( + bytes calldata _packetBytes, + bytes memory _options, + bytes32 _guid, + address _composer + ) external payable { + this.lzCompose( + _packetBytes.dstEid(), + _packetBytes.receiverB20(), + _options, + _guid, + _composer, + _packetBytes.message() + ); + } + + // @dev the verifyPackets does not know the composeMsg if it is NOT the same as the original lzReceive payload + // Can call this directly from your test to lzCompose those types of packets + function lzCompose( + uint32 _dstEid, + address _from, + bytes memory _options, + bytes32 _guid, + address _to, + bytes calldata _composerMsg + ) external payable { + EndpointV2 endpoint = EndpointV2(endpoints[_dstEid]); + (uint16 index, uint256 gas, uint256 value) = _parseExecutorLzComposeOption(_options); + endpoint.lzCompose{ value: value, gas: gas }(_from, _to, _guid, index, _composerMsg, bytes("")); + } + + function validatePacket(bytes calldata _packetBytes) external { + uint32 dstEid = _packetBytes.dstEid(); + EndpointV2 endpoint = EndpointV2(endpoints[dstEid]); + (address receiveLib, ) = endpoint.getReceiveLibrary(_packetBytes.receiverB20(), _packetBytes.srcEid()); + ReceiveUln302 dstUln = ReceiveUln302(receiveLib); + + (uint64 major, , ) = IMessageLib(receiveLib).version(); + if (major == 3) { + // it is ultra light node + bytes memory config = dstUln.getConfig(_packetBytes.srcEid(), _packetBytes.receiverB20(), 2); // CONFIG_TYPE_ULN + DVN dvn = DVN(abi.decode(config, (UlnConfig)).requiredDVNs[0]); + + bytes memory packetHeader = _packetBytes.header(); + bytes32 payloadHash = keccak256(_packetBytes.payload()); + + // sign + bytes memory signatures; + bytes memory verifyCalldata = abi.encodeWithSelector( + IReceiveUlnE2.verify.selector, + packetHeader, + payloadHash, + 100 + ); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), verifyCalldata, 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + ExecuteParam[] memory params = new ExecuteParam[](1); + params[0] = ExecuteParam(dstEid, address(dstUln), verifyCalldata, 1000, signatures); + dvn.execute(params); + + // commit verification + bytes memory callData = abi.encodeWithSelector( + IReceiveUlnE2.commitVerification.selector, + packetHeader, + payloadHash + ); + { + bytes32 hash = dvn.hashCallData(dstEid, address(dstUln), callData, 1000); + bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, ethSignedMessageHash); // matches dvn signer + signatures = abi.encodePacked(r, s, v); + } + params[0] = ExecuteParam(dstEid, address(dstUln), callData, 1000, signatures); + dvn.execute(params); + } else { + SimpleMessageLibMock(payable(receiveLib)).validatePacket(_packetBytes); + } + } + + function assertGuid(bytes calldata packetBytes, bytes32 guid) external pure { + bytes32 packetGuid = packetBytes.guid(); + require(packetGuid == guid, "guid not match"); + } + + function registerEndpoint(EndpointV2 endpoint) public { + endpoints[endpoint.eid()] = address(endpoint); + } + + function hasPendingPackets(uint16 _dstEid, bytes32 _dstAddress) public view returns (bool flag) { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + return queue.length() > 0; + } + + function getNextInflightPacket(uint16 _dstEid, bytes32 _dstAddress) public view returns (bytes memory packetBytes) { + DoubleEndedQueue.Bytes32Deque storage queue = packetsQueue[_dstEid][_dstAddress]; + if (queue.length() > 0) { + bytes32 guid = queue.back(); + packetBytes = packets[guid]; + } + } + + function addressToBytes32(address _addr) internal pure returns (bytes32) { + return bytes32(uint256(uint160(_addr))); + } + + receive() external payable {} +} diff --git a/oapp/test/hardhat-demo.test.ts b/oapp/test/hardhat-demo.test.ts new file mode 100644 index 0000000..0e79803 --- /dev/null +++ b/oapp/test/hardhat-demo.test.ts @@ -0,0 +1,7 @@ +import { expect } from 'chai' + +describe('Hardhat UnitTest Demo', function () { + it('should pass', async function () { + expect(true).equal(true) + }) +}) diff --git a/oapp/test/mocks/ERC20Mock.sol b/oapp/test/mocks/ERC20Mock.sol new file mode 100644 index 0000000..6f0e76c --- /dev/null +++ b/oapp/test/mocks/ERC20Mock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.22; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } +} diff --git a/oapp/test/mocks/ExecutorFeeLibMock.sol b/oapp/test/mocks/ExecutorFeeLibMock.sol new file mode 100644 index 0000000..906ef94 --- /dev/null +++ b/oapp/test/mocks/ExecutorFeeLibMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.22; + +import { ExecutorFeeLib } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/ExecutorFeeLib.sol"; + +contract ExecutorFeeLibMock is ExecutorFeeLib { + constructor() ExecutorFeeLib(1e18) {} + + function _isV1Eid(uint32 /*_eid*/) internal pure override returns (bool) { + return false; + } +} diff --git a/oapp/test/mocks/OFTAdapterMock.sol b/oapp/test/mocks/OFTAdapterMock.sol new file mode 100644 index 0000000..019d962 --- /dev/null +++ b/oapp/test/mocks/OFTAdapterMock.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { OFTAdapter } from "../../contracts/oft/OFTAdapter.sol"; + +contract OFTAdapterMock is OFTAdapter { + constructor(address _token, address _lzEndpoint, address _owner) OFTAdapter(_token, _lzEndpoint, _owner) {} + + // @dev expose internal functions for testing purposes + function debit( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debit(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function debitView( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public view returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { + return _credit(_to, _amountToCreditLD, _srcEid); + } + + function increaseOutboundAmount(uint256 _amount) public { + outboundAmount += _amount; + } + + function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { + return _removeDust(_amountLD); + } +} diff --git a/oapp/test/mocks/OFTComposerMock.sol b/oapp/test/mocks/OFTComposerMock.sol new file mode 100644 index 0000000..6a18d26 --- /dev/null +++ b/oapp/test/mocks/OFTComposerMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IOAppComposer } from "../../contracts/oapp/interfaces/IOAppComposer.sol"; + +contract OFTComposerMock is IOAppComposer { + // default empty values for testing a lzCompose received message + address public from; + bytes32 public guid; + bytes public message; + address public executor; + bytes public extraData; + + function lzCompose( + address _from, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata /*_extraData*/ + ) external payable { + from = _from; + guid = _guid; + message = _message; + executor = _executor; + extraData = _message; + } +} diff --git a/oapp/test/mocks/OFTInspectorMock.sol b/oapp/test/mocks/OFTInspectorMock.sol new file mode 100644 index 0000000..b67818a --- /dev/null +++ b/oapp/test/mocks/OFTInspectorMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { IOAppMsgInspector } from "../../contracts/oapp/interfaces/IOAppMsgInspector.sol"; + +contract OFTInspectorMock is IOAppMsgInspector { + function inspect(bytes calldata _message, bytes calldata _options) external pure returns (bool) { + revert InspectionFailed(_message, _options); + } +} diff --git a/oapp/test/mocks/OFTMock.sol b/oapp/test/mocks/OFTMock.sol new file mode 100644 index 0000000..fe48d59 --- /dev/null +++ b/oapp/test/mocks/OFTMock.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { OFT } from "../../contracts/oft/OFT.sol"; +import { SendParam } from "../../contracts/oft/OFTCore.sol"; + +contract OFTMock is OFT { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _owner + ) OFT(_name, _symbol, _lzEndpoint, _owner) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + // @dev expose internal functions for testing purposes + function debit( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debit(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function debitView( + uint256 _amountToSendLD, + uint256 _minAmountToCreditLD, + uint32 _dstEid + ) public view returns (uint256 amountDebitedLD, uint256 amountToCreditLD) { + return _debitView(_amountToSendLD, _minAmountToCreditLD, _dstEid); + } + + function removeDust(uint256 _amountLD) public view returns (uint256 amountLD) { + return _removeDust(_amountLD); + } + + function toLD(uint64 _amountSD) public view returns (uint256 amountLD) { + return _toLD(_amountSD); + } + + function toSD(uint256 _amountLD) public view returns (uint64 amountSD) { + return _toSD(_amountLD); + } + + function credit(address _to, uint256 _amountToCreditLD, uint32 _srcEid) public returns (uint256 amountReceivedLD) { + return _credit(_to, _amountToCreditLD, _srcEid); + } + + function buildMsgAndOptions( + SendParam calldata _sendParam, + bytes calldata _extraOptions, + bytes calldata _composeMsg, + uint256 _amountToCreditLD + ) public view returns (bytes memory message, bytes memory options) { + return _buildMsgAndOptions(_sendParam, _extraOptions, _composeMsg, _amountToCreditLD); + } +} diff --git a/oapp/test/mocks/PreCrimeV2Mock.sol b/oapp/test/mocks/PreCrimeV2Mock.sol new file mode 100644 index 0000000..37c3fce --- /dev/null +++ b/oapp/test/mocks/PreCrimeV2Mock.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { PreCrimePeer } from "../../contracts/precrime/interfaces/IPreCrime.sol"; +import { IOAppPreCrimeSimulator } from "../../contracts/precrime/interfaces/IOAppPreCrimeSimulator.sol"; +import { PreCrime } from "../../contracts/precrime/PreCrime.sol"; +import { InboundPacket } from "../../contracts/precrime/libs/Packet.sol"; + +import { PreCrimeV2SimulatorMock } from "./PreCrimeV2SimulatorMock.sol"; + +contract PreCrimeV2Mock is PreCrime { + constructor(address _endpoint, address _simulator) PreCrime(_endpoint, _simulator, msg.sender) {} + + uint32[] public eids; + bytes[] public results; + + function buildSimulationResult() external view override returns (bytes memory) { + return abi.encode(PreCrimeV2SimulatorMock(simulator).count()); + } + + function _getPreCrimePeers( + InboundPacket[] memory _packets + ) internal view override returns (PreCrimePeer[] memory peers) { + for (uint256 i = 0; i < _packets.length; i++) { + InboundPacket memory packet = _packets[i]; + if (IOAppPreCrimeSimulator(simulator).isPeer(packet.origin.srcEid, packet.origin.sender)) { + return preCrimePeers; + } + } + return (new PreCrimePeer[](0)); + } + + function _preCrime(InboundPacket[] memory, uint32[] memory _eids, bytes[] memory _results) internal override { + eids = _eids; + results = _results; + } +} diff --git a/oapp/test/mocks/PreCrimeV2SimulatorMock.sol b/oapp/test/mocks/PreCrimeV2SimulatorMock.sol new file mode 100644 index 0000000..79db322 --- /dev/null +++ b/oapp/test/mocks/PreCrimeV2SimulatorMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Origin } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; + +import { OAppPreCrimeSimulator } from "../../contracts/precrime/OAppPreCrimeSimulator.sol"; + +contract PreCrimeV2SimulatorMock is OAppPreCrimeSimulator { + uint256 public count; + + error InvalidEid(); + + function _lzReceiveSimulate( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) internal override { + if (_origin.srcEid == 0) revert InvalidEid(); + count++; + } + + function isPeer(uint32 _eid, bytes32 _peer) public pure override returns (bool) { + return bytes32(uint256(_eid)) == _peer; + } +} diff --git a/oapp/test/mocks/SendUln302Mock.sol b/oapp/test/mocks/SendUln302Mock.sol new file mode 100644 index 0000000..2524cfb --- /dev/null +++ b/oapp/test/mocks/SendUln302Mock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { Packet } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ISendLib.sol"; +import { MessagingFee } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import { SendUln302 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln/uln302/SendUln302.sol"; + +import { TestHelper } from "../TestHelper.sol"; + +contract SendUln302Mock is SendUln302 { + // offchain packets schedule + TestHelper public testHelper; + + constructor( + address payable _verifyHelper, + address _endpoint, + uint256 _treasuryGasCap, + uint256 _treasuryGasForFeeCap + ) SendUln302(_endpoint, _treasuryGasCap, _treasuryGasForFeeCap) { + testHelper = TestHelper(_verifyHelper); + } + + function send( + Packet calldata _packet, + bytes calldata _options, + bool _payInLzToken + ) public override returns (MessagingFee memory fee, bytes memory encodedPacket) { + (fee, encodedPacket) = super.send(_packet, _options, _payInLzToken); + testHelper.schedulePacket(encodedPacket, _options); + } +} diff --git a/oapp/test/mocks/SimpleMessageLibMock.sol b/oapp/test/mocks/SimpleMessageLibMock.sol new file mode 100644 index 0000000..344832e --- /dev/null +++ b/oapp/test/mocks/SimpleMessageLibMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import { SimpleMessageLib } from "@layerzerolabs/lz-evm-protocol-v2/contracts/messagelib/SimpleMessageLib.sol"; + +import { TestHelper } from "../TestHelper.sol"; + +contract SimpleMessageLibMock is SimpleMessageLib { + // offchain packets schedule + TestHelper public testHelper; + + constructor(address payable _verifyHelper, address _endpoint) SimpleMessageLib(_endpoint, address(0x0)) { + testHelper = TestHelper(_verifyHelper); + } + + function _handleMessagingParamsHook(bytes memory _encodedPacket, bytes memory _options) internal override { + testHelper.schedulePacket(_encodedPacket, _options); + } +} diff --git a/package.json b/package.json index 51ee664..a2809fa 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "private": true, "workspaces": [ "protocol", - "messagelib" + "messagelib", + "oapp" ], "scripts": { "clean": "$npm_execpath workspaces foreach --all run clean", diff --git a/yarn.lock b/yarn.lock index eebfc97..89f1dd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -643,7 +643,7 @@ __metadata: languageName: node linkType: hard -"@layerzerolabs/lz-evm-messagelib-v2@workspace:messagelib": +"@layerzerolabs/lz-evm-messagelib-v2@workspace:^, @layerzerolabs/lz-evm-messagelib-v2@workspace:messagelib": version: 0.0.0-use.local resolution: "@layerzerolabs/lz-evm-messagelib-v2@workspace:messagelib" dependencies: @@ -657,6 +657,23 @@ __metadata: languageName: unknown linkType: soft +"@layerzerolabs/lz-evm-oapp-v2@workspace:oapp": + version: 0.0.0-use.local + resolution: "@layerzerolabs/lz-evm-oapp-v2@workspace:oapp" + dependencies: + "@layerzerolabs/lz-evm-messagelib-v2": "workspace:^" + "@layerzerolabs/lz-evm-protocol-v2": "workspace:^" + "@layerzerolabs/lz-evm-v1-0.7": "npm:^1.5.77" + "@openzeppelin/contracts": "npm:^4.8.1" + "@openzeppelin/contracts-upgradeable": "npm:^4.8.1" + hardhat-deploy: "npm:^0.11.44" + rimraf: "npm:^5.0.5" + solidity-bytes-utils: "npm:^0.8.0" + peerDependencies: + solidity-bytes-utils: ^0.8.0 + languageName: unknown + linkType: soft + "@layerzerolabs/lz-evm-protocol-v2@workspace:^, @layerzerolabs/lz-evm-protocol-v2@workspace:protocol": version: 0.0.0-use.local resolution: "@layerzerolabs/lz-evm-protocol-v2@workspace:protocol"