Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add more Foundry tests, including ABA, lzCompose #1226

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions packages/test-devtools-evm-foundry/contracts/mocks/ABAMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.22;

import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title ABAMock contract for demonstrating LayerZero messaging between blockchains.
* @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
* @dev This contract showcases a PingPong style call (A -> B -> A) using LayerZero's OApp Standard.
*/
contract ABAMock is OApp, OAppOptionsType3 {

/// @notice Last received message data.
string public data = "Nothing received yet";

/// @notice Message types that are used to identify the various OApp operations.
/// @dev These values are used in things like combineOptions() in OAppOptionsType3.
uint16 public constant SEND = 1;
uint16 public constant SEND_ABA = 2;

/// @notice Emitted when a return message is successfully sent (B -> A).
event ReturnMessageSent(string message, uint32 dstEid);

/// @notice Emitted when a message is received from another chain.
event MessageReceived(string message, uint32 senderEid, bytes32 sender);

/// @notice Emitted when a message is sent to another chain (A -> B).
event MessageSent(string message, uint32 dstEid);

/// @dev Revert with this error when an invalid message type is used.
error InvalidMsgType();

/**
* @dev Constructs a new PingPong contract instance.
* @param _endpoint The LayerZero endpoint for this contract to interact with.
* @param _owner The owner address that will be set as the owner of the contract.
*/
constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(msg.sender) {}

function encodeMessage(string memory _message, uint16 _msgType, bytes memory _extraReturnOptions) public pure returns (bytes memory) {
// Get the length of _extraReturnOptions
uint256 extraOptionsLength = _extraReturnOptions.length;

// Encode the entire message, prepend and append the length of extraReturnOptions
return abi.encode(_message, _msgType, extraOptionsLength, _extraReturnOptions, extraOptionsLength);
}

/**
* @notice Returns the estimated messaging fee for a given message.
* @param _dstEid Destination endpoint ID where the message will be sent.
* @param _msgType The type of message being sent.
* @param _message The message content.
* @param _extraSendOptions Gas options for receiving the send call (A -> B).
* @param _extraReturnOptions Additional gas options for the return call (B -> A).
* @param _payInLzToken Boolean flag indicating whether to pay in LZ token.
* @return fee The estimated messaging fee.
*/
function quote(
uint32 _dstEid,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions,
bytes calldata _extraReturnOptions,
bool _payInLzToken
) public view returns (MessagingFee memory fee) {
bytes memory payload = encodeMessage(_message, _msgType, _extraReturnOptions);
bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions);
fee = _quote(_dstEid, payload, options, _payInLzToken);
}


/**
* @notice Sends a message to a specified destination chain.
* @param _dstEid Destination endpoint ID for the message.
* @param _msgType The type of message to send.
* @param _message The message content.
* @param _extraSendOptions Options for sending the message, such as gas settings.
* @param _extraReturnOptions Additional options for the return message.
*/
function send(
uint32 _dstEid,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions, // gas settings for A -> B
bytes calldata _extraReturnOptions // gas settings for B -> A
) external payable {
// Encodes the message before invoking _lzSend.
require(bytes(_message).length <= 32, "String exceeds 32 bytes");

if (_msgType != SEND && _msgType != SEND_ABA) {
revert InvalidMsgType();
}

bytes memory options = combineOptions(_dstEid, _msgType, _extraSendOptions);

_lzSend(
_dstEid,
encodeMessage(_message, _msgType, _extraReturnOptions),
options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed source message.
payable(msg.sender)
);

emit MessageSent(_message, _dstEid);
}

function decodeMessage(bytes calldata encodedMessage) public pure returns (string memory message, uint16 msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) {
extraOptionsStart = 256; // Starting offset after _message, _msgType, and extraOptionsLength
string memory _message;
uint16 _msgType;

// Decode the first part of the message
(_message, _msgType, extraOptionsLength) = abi.decode(encodedMessage, (string, uint16, uint256));

return (_message, _msgType, extraOptionsStart, extraOptionsLength);
}

/**
* @notice Internal function to handle receiving messages from another chain.
* @dev Decodes and processes the received message based on its type.
* @param _origin Data about the origin of the received message.
* @param message The received message content.
*/
function _lzReceive(
Origin calldata _origin,
bytes32 /*guid*/,
bytes calldata message,
address, // Executor address as specified by the OApp.
bytes calldata // Any extra data or options to trigger on receipt.
) internal override {

(string memory _data, uint16 _msgType, uint256 extraOptionsStart, uint256 extraOptionsLength) = decodeMessage(message);
data = _data;

if (_msgType == SEND_ABA) {

string memory _newMessage = "Chain B says goodbye!";

bytes memory _options = combineOptions(_origin.srcEid, SEND, message[extraOptionsStart:extraOptionsStart + extraOptionsLength]);

_lzSend(
_origin.srcEid,
abi.encode(_newMessage, SEND),
// Future additions should make the data types static so that it is easier to find the array locations.
_options,
// Fee in native gas and ZRO token.
MessagingFee(msg.value, 0),
// Refund address in case of failed send call.
// @dev Since the Executor makes the return call, this contract is the refund address.
payable(address(this))
);

emit ReturnMessageSent(_newMessage, _origin.srcEid);
}

emit MessageReceived(data, _origin.srcEid, _origin.sender);
}


receive() external payable {}

}
130 changes: 130 additions & 0 deletions packages/test-devtools-evm-foundry/contracts/mocks/BatchSendMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.22;

import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title BatchSendMock contract for demonstrating multiple outbound cross-chain calls using LayerZero.
* @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
* @dev This contract showcases how to send multiple cross-chain calls with one source function call using LayerZero's OApp Standard.
*/
contract BatchSendMock is OApp, OAppOptionsType3 {
/// @notice Last received message data.
string public data = "Nothing received yet";

/// @notice Message types that are used to identify the various OApp operations.
/// @dev These values are used in things like combineOptions() in OAppOptionsType3 (enforcedOptions).
uint16 public constant SEND = 1;

/// @notice Emitted when a message is received from another chain.
event MessageReceived(string message, uint32 senderEid, bytes32 sender);

/// @notice Emitted when a message is sent to another chain (A -> B).
event MessageSent(string message, uint32 dstEid);

/// @dev Revert with this error when an invalid message type is used.
error InvalidMsgType();

/**
* @dev Constructs a new BatchSend contract instance.
* @param _endpoint The LayerZero endpoint for this contract to interact with.
* @param _owner The owner address that will be set as the owner of the contract.
*/
constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(msg.sender) {}

function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) {
if (msg.value < _nativeFee) revert NotEnoughNative(msg.value);
return _nativeFee;
}

/**
* @notice Returns the estimated messaging fee for a given message.
* @param _dstEids Destination endpoint ID array where the message will be batch sent.
* @param _msgType The type of message being sent.
* @param _message The message content.
* @param _extraSendOptions Extra gas options for receiving the send call (A -> B).
* Will be summed with enforcedOptions, even if no enforcedOptions are set.
* @param _payInLzToken Boolean flag indicating whether to pay in LZ token.
* @return totalFee The estimated messaging fee for sending to all pathways.
*/
function quote(
uint32[] memory _dstEids,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions,
bool _payInLzToken
) public view returns (MessagingFee memory totalFee) {
bytes memory encodedMessage = abi.encode(_message);

for (uint i = 0; i < _dstEids.length; i++) {
bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions);
MessagingFee memory fee = _quote(_dstEids[i], encodedMessage, options, _payInLzToken);
totalFee.nativeFee += fee.nativeFee;
totalFee.lzTokenFee += fee.lzTokenFee;
}
}

function send(
uint32[] memory _dstEids,
uint16 _msgType,
string memory _message,
bytes calldata _extraSendOptions // gas settings for A -> B
) external payable {
if (_msgType != SEND) {
revert InvalidMsgType();
}

// Calculate the total messaging fee required.
MessagingFee memory totalFee = quote(_dstEids, _msgType, _message, _extraSendOptions, false);
require(msg.value >= totalFee.nativeFee, "Insufficient fee provided");

// Encodes the message before invoking _lzSend.
bytes memory _encodedMessage = abi.encode(_message);

uint256 totalNativeFeeUsed = 0;
uint256 remainingValue = msg.value;

for (uint i = 0; i < _dstEids.length; i++) {
bytes memory options = combineOptions(_dstEids[i], _msgType, _extraSendOptions);
MessagingFee memory fee = _quote(_dstEids[i], _encodedMessage, options, false);

totalNativeFeeUsed += fee.nativeFee;
remainingValue -= fee.nativeFee;

// Ensure the current call has enough allocated fee from msg.value.
require(remainingValue >= 0, "Insufficient fee for this destination");

_lzSend(
_dstEids[i],
_encodedMessage,
options,
fee,
payable(msg.sender)
);

emit MessageSent(_message, _dstEids[i]);
}
}

/**
* @notice Internal function to handle receiving messages from another chain.
* @dev Decodes and processes the received message based on its type.
* @param _origin Data about the origin of the received message.
* @param message The received message content.
*/
function _lzReceive(
Origin calldata _origin,
bytes32 /*guid*/,
bytes calldata message,
address, // Executor address as specified by the OApp.
bytes calldata // Any extra data or options to trigger on receipt.
) internal override {
string memory _data = abi.decode(message, (string));
data = _data;

emit MessageReceived(data, _origin.srcEid, _origin.sender);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IOAppComposer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";

/// @title ComposerMock Contract
/// @dev This contract mocks an ERC20 token swap in response to an OFT being received (lzReceive) on the destination chain to this address.
/// @notice THIS IS AN EXAMPLE CONTRACT. DO NOT USE THIS CODE IN PRODUCTION.
/// @notice The contract is designed to interact with LayerZero's Omnichain Fungible Token (OFT) Standard,
/// allowing it to respond to cross-chain OFT mint events with a token swap (erc20.safeTransfer()) action.
contract ComposerMock is IOAppComposer {
using SafeERC20 for IERC20;
IERC20 public erc20;
address public immutable endpoint;
address public immutable oApp;

/// @notice Emitted when a token swap is executed.
/// @param user The address of the user who receives the swapped tokens.
/// @param tokenOut The address of the ERC20 token being swapped.
/// @param amount The amount of tokens swapped.
event Swapped(address indexed user, address tokenOut, uint256 amount);

/// @notice Constructs the SwapMock contract.
/// @dev Initializes the contract with a specific ERC20 token address.
/// @param _erc20 The address of the ERC20 token that will be used in swaps.
constructor(address _erc20, address _endpoint, address _oApp) {
erc20 = IERC20(_erc20);
endpoint = _endpoint;
oApp = _oApp;
}

/// @notice Handles incoming composed messages from LayerZero.
/// @dev Decodes the message payload to perform a token swap.
/// This method expects the encoded compose message to contain the swap amount and recipient address.
/// @param _oApp The address of the originating OApp.
/// @param /*_guid*/ The globally unique identifier of the message (unused in this mock).
/// @param _message The encoded message content, expected to be of type: (address receiver).
/// @param /*Executor*/ Executor address (unused in this mock).
/// @param /*Executor Data*/ Additional data for checking for a specific executor (unused in this mock).
function lzCompose(
address _oApp,
bytes32 /*_guid*/,
bytes calldata _message,
address /*Executor*/,
bytes calldata /*Executor Data*/
) external payable override {
require(_oApp == oApp, "!oApp");
require(msg.sender == endpoint, "!endpoint");
// Extract the composed message from the delivered message using the MsgCodec
address _receiver = abi.decode(OFTComposeMsgCodec.composeMsg(_message), (address));
uint256 _amountLD = OFTComposeMsgCodec.amountLD(_message);
// Execute the token swap by transferring the specified amount to the receiver
erc20.safeTransfer(_receiver, _amountLD);

// Emit an event to log the token swap details
emit Swapped(_receiver, address(erc20), _amountLD);
}
}
Loading