Skip to content

Commit

Permalink
chore: add UniswapV3Composer unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
St0rmBr3w committed Dec 11, 2024
1 parent 8381e68 commit a055ca5
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTCom

/**
* @title UniswapV3Composer
* @author
*
* @notice Handles cross-chain OFT token swaps using Uniswap V3 upon receiving tokens via LayerZero.
*
* @dev This contract inherits from IOAppComposer and interacts with Uniswap V3's SwapRouter to execute token swaps.
*/
contract UniswapV3Composer is IOAppComposer {
Expand Down
249 changes: 249 additions & 0 deletions examples/oft-composer-library/test/foundry/UniswapV3Composer.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

// Import necessary testing libraries and contracts
import "forge-std/Test.sol";
import { UniswapV3Composer } from "../../contracts/UniswapV3Composer.sol";
import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

// Mock imports
import { OFTMock } from "../mocks/OFTMock.sol";
import { ERC20Mock } from "../mocks/ERC20Mock.sol";
import { SwapRouterMock } from "../mocks/SwapRouterMock.sol";

// OApp imports
import { IOAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";

// OFT imports
import { IOFT, SendParam, OFTReceipt } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";
import { MessagingFee, MessagingReceipt } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol";
import { OFTComposeMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
import { OFTMsgCodec } from "@layerzerolabs/oft-evm/contracts/libs/OFTMsgCodec.sol";

// OpenZeppelin imports
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

// Forge imports
import "forge-std/console.sol";

// DevTools imports
import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol";

/**
* @title UniswapV3ComposerTest
* @notice Unit tests for the UniswapV3Composer contract.
* @dev Utilizes Forge's testing framework to simulate interactions and verify contract behavior.
*/
contract UniswapV3ComposerTest is TestHelperOz5 {
using OptionsBuilder for bytes;

// ----------------------------
// ============ Setup ===========
// ----------------------------

// Endpoint Identifiers
uint32 private constant aEid = 1;
uint32 private constant bEid = 2;

// Mock Contracts
OFTMock private aOFT;
OFTMock private bOFT;
UniswapV3Composer private bComposer;
SwapRouterMock private bSwapRouter;

// ERC20 Tokens
address bTokenIn;
ERC20Mock private bTokenOut;

// User Addresses
address private userA = makeAddr("userA");
address private userB = makeAddr("userB");
address private otherOFTB = makeAddr("otherOFTB");
address private receiver = makeAddr("receiver");

// Initial Balances and Swap Amounts
uint256 private constant initialBalance = 100 ether;
uint256 private constant swapAmountIn = 1 ether;
uint256 private constant swapAmountOut = 1 ether; // Predefined output for SwapRouterMock

// Events
event SwapExecuted(address indexed user, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);

/**
* @notice Sets up the testing environment before each test.
*
* @dev Deploys mock contracts, initializes token balances, and configures the UniswapV3Composer.
*/
function setUp() public virtual override {
// Allocate Ether to users
vm.deal(userA, 1000 ether);
vm.deal(userB, 1000 ether);

// Initialize endpoints
super.setUp();
setUpEndpoints(2, LibraryType.UltraLightNode);

// Deploy mock ERC20 TokenOut and TokenIn
bTokenOut = new ERC20Mock("TokenOut", "OUT");

// Deploy mock OFTs with corresponding endpoints
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)))
);

// Every OFT variant has token() to distinguish between OFT Adapter and OFT
bTokenIn = address(bOFT.token());

// Deploy mock SwapRouter with correct tokenIn and tokenOut addresses
bSwapRouter = new SwapRouterMock(address(bTokenIn), address(bTokenOut), swapAmountOut);

// Deploy the UniswapV3Composer contract with initialized SwapRouter and OFT
bComposer = new UniswapV3Composer(address(bSwapRouter), address(endpoints[bEid]), address(bOFT));

// Configure and link the deployed OFTs
address[] memory ofts = new address[](2);
ofts[0] = address(aOFT);
ofts[1] = address(bOFT);
this.wireOApps(ofts);

// Mint initial tokens to users and contracts
aOFT.mint(userA, initialBalance);
bOFT.mint(userB, initialBalance);
bOFT.mint(address(bComposer), initialBalance);
// Note: SwapRouterMock handles minting TokenOut during swap execution
}

// ----------------------------
// ========= Constructor =======
// ----------------------------

/**
* @notice Tests the constructor of UniswapV3Composer for correct initialization.
* @dev Verifies that SwapRouter, endpoint, and OFT addresses are set as expected.
*/
function test_constructor() public {
// Assert that the SwapRouter address is correctly set in UniswapV3Composer
assertEq(address(bComposer.swapRouter()), address(bSwapRouter), "SwapRouter address mismatch");

// Assert that the endpoint address is correctly set in UniswapV3Composer
assertEq(bComposer.endpoint(), address(endpoints[bEid]), "Endpoint address mismatch");

// Assert that the OFT address is correctly set in UniswapV3Composer
assertEq(bComposer.oft(), address(bOFT), "OFT address mismatch");
}

// ----------------------------
// ========= Test Cases =======
// ----------------------------

/**
* @notice Tests the `lzCompose` function to ensure it correctly handles incoming messages and executes swaps.
* @dev Simulates sending a compose message via LayerZero and verifies SwapRouterMock interactions and token balances.
*/
function test_lzCompose() public {
// ------------------------------
// 1. Prepare the Compose Message
// ------------------------------
uint24 fee = 3000; // Example fee tier

// Encode the compose message with (userA, bTokenOut, fee, receiver)
bytes memory composeMsg = abi.encode(userA, address(bTokenOut), fee, receiver);

// Encode the full message using OFTComposeMsgCodec
// Parameters:
// _nonce: 1 (unique identifier)
// _srcEid: aEid (source endpoint ID)
// _amountLD: swapAmountIn (amount to be swapped)
// _composeMsg: composeMsg (encoded compose message)
bytes memory fullMessage = OFTComposeMsgCodec.encode(
1,
aEid,
swapAmountIn,
abi.encodePacked(addressToBytes32(userA), composeMsg)
);

// ------------------------------
// 2. Simulate Sending the Message
// ------------------------------
// Prank as the authorized endpoint to call lzCompose
vm.prank(address(endpoints[bEid]));

// Execute lzCompose with the encoded full message
bComposer.lzCompose(
address(bOFT),
bytes32(0), // guid (unused in this test)
fullMessage,
address(this), // executor
bytes("") // extraData (unused in this test)
);

// ------------------------------
// 3. Verify SwapRouterMock Interactions
// ------------------------------
assertEq(bSwapRouter.lastSender(), address(bComposer), "SwapRouter sender mismatch");
assertEq(bSwapRouter.lastTokenIn(), address(bTokenIn), "TokenIn address mismatch");
assertEq(bSwapRouter.lastTokenOut(), address(bTokenOut), "TokenOut address mismatch");
assertEq(bSwapRouter.lastFee(), fee, "Fee tier mismatch");
assertEq(bSwapRouter.lastRecipient(), receiver, "Recipient address mismatch");
assertEq(bSwapRouter.lastAmountIn(), swapAmountIn, "AmountIn mismatch");
assertEq(bSwapRouter.lastAmountOut(), swapAmountOut, "AmountOut mismatch");

// ------------------------------
// 4. Verify Token Balances After Swap
// ------------------------------
// Verify that bComposer's tokenIn balance decreased by swapAmountIn
assertEq(
IERC20(bOFT.token()).balanceOf(address(bComposer)),
initialBalance - swapAmountIn,
"bComposer TokenIn balance incorrect"
);

// Verify that the receiver's tokenOut balance increased by swapAmountOut
assertEq(bTokenOut.balanceOf(receiver), swapAmountOut, "Receiver TokenOut balance incorrect");
}

/**
* @notice Tests that `lzCompose` reverts when called with an unauthorized OFT.
* @dev Attempts to invoke `lzCompose` with a different OFT address and expects a revert.
*/
function test_lzCompose_UnauthorizedOFT() public {
// ------------------------------
// 1. Prepare the Unauthorized Compose Message
// ------------------------------
uint24 fee = 3000; // Example fee tier

// Encode the compose message with (userA, bTokenOut, fee, receiver)
bytes memory composeMsg = abi.encode(userA, address(bTokenOut), fee, receiver);

// Encode the full message using OFTComposeMsgCodec
bytes memory fullMessage = OFTComposeMsgCodec.encode(
1, // _nonce
aEid, // _srcEid
swapAmountIn, // _amountLD
composeMsg // _composeMsg
);

// ------------------------------
// 2. Attempt Unauthorized lzCompose
// ------------------------------
// Prank as the authorized endpoint to call lzCompose
vm.prank(address(endpoints[bEid]));

// Expect the transaction to revert with "Unauthorized OFT"
vm.expectRevert("Unauthorized OFT");

// Attempt to execute lzCompose with an unauthorized OFT address
bComposer.lzCompose(
address(otherOFTB),
bytes32(0), // guid (unused in this test)
fullMessage,
address(0), // executor
bytes("") // extraData (unused in this test)
);
}
}
123 changes: 123 additions & 0 deletions examples/oft-composer-library/test/mocks/SwapRouterMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Import the ISwapRouter interface from Uniswap V3 Periphery
import { ISwapRouter } from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";

// Import the ERC20Mock to simulate token behavior
import { ERC20Mock } from "../mocks/ERC20Mock.sol";
import { OFTMock } from "../mocks/OFTMock.sol";

/**
* @title SwapRouterMock
* @notice A mock implementation of Uniswap V3's ISwapRouter for testing purposes.
* @dev This contract records the parameters of the last swap and returns a predefined amountOut.
*/
contract SwapRouterMock is ISwapRouter {
// State variables to record the parameters of the last swap
address public lastSender;
address public lastTokenIn;
address public lastTokenOut;
uint24 public lastFee;
address public lastRecipient;
uint256 public lastAmountIn;
uint256 public lastAmountOut;

// ERC20 tokens used for swapping
OFTMock public tokenIn;
ERC20Mock public tokenOut;

// Predefined amountOut to return on swaps
uint256 private predefinedAmountOut;

/**
* @notice Initializes the SwapRouterMock with predefined tokens and amountOut.
* @param _tokenIn The ERC20 token address being swapped from.
* @param _tokenOut The ERC20 token address being swapped to.
* @param _predefinedAmountOut The amount of tokenOut to return on swaps.
*/
constructor(address _tokenIn, address _tokenOut, uint256 _predefinedAmountOut) {
tokenIn = OFTMock(_tokenIn);
tokenOut = ERC20Mock(_tokenOut);
predefinedAmountOut = _predefinedAmountOut;
}

/**
* @notice Allows setting a new predefined amountOut for subsequent swaps.
* @param _newAmountOut The new amountOut to return.
*/
function setPredefinedAmountOut(uint256 _newAmountOut) external {
predefinedAmountOut = _newAmountOut;
}

/**
* @notice Mocks the exactInputSingle function of Uniswap V3's ISwapRouter.
* @param params The parameters for the swap, as defined in ISwapRouter.ExactInputSingleParams.
* @return amountOut The amount of tokenOut received from the swap.
*
* @dev This function records the swap parameters and returns a predefined amountOut.
* It also simulates the token transfer by minting tokenOut to the recipient.
*/
function exactInputSingle(
ExactInputSingleParams calldata params
) external payable override returns (uint256 amountOut) {
// Validate amountIn
require(params.amountIn > 0, "SwapRouterMock: amountIn must be greater than zero");

// Record the parameters of the swap
lastSender = msg.sender;
lastTokenIn = params.tokenIn;
lastTokenOut = params.tokenOut;
lastFee = params.fee;
lastRecipient = params.recipient;
lastAmountIn = params.amountIn;
lastAmountOut = predefinedAmountOut;

// Simulate the transfer of tokenIn from the sender to the SwapRouterMock
tokenIn.transferFrom(msg.sender, address(this), params.amountIn);

// Simulate minting tokenOut to the recipient
tokenOut.mint(params.recipient, predefinedAmountOut);

// Return the predefined amountOut
return predefinedAmountOut;
}

/**
* @notice Mocks other functions from the ISwapRouter interface.
* @dev These functions are left unimplemented and will revert if called, indicating they are not supported in the mock.
*/

// Swaps with exact input along a specified path
function exactInput(
ISwapRouter.ExactInputParams calldata /*params*/
) external payable override returns (uint256 /*amountOut*/) {
revert("SwapRouterMock: exactInput not implemented");
}

// Swaps to receive an exact amount of output tokens with single-hop
function exactOutputSingle(
ISwapRouter.ExactOutputSingleParams calldata /*params*/
) external payable override returns (uint256 /*amountIn*/) {
revert("SwapRouterMock: exactOutputSingle not implemented");
}

// Swaps to receive an exact amount of output tokens along a specified path
function exactOutput(
ISwapRouter.ExactOutputParams calldata /*params*/
) external payable override returns (uint256 /*amountIn*/) {
revert("SwapRouterMock: exactOutput not implemented");
}

/**
* @notice Mocks the uniswapV3SwapCallback function from IUniswapV3SwapCallback.
* @dev This mock does not handle actual swap callbacks and will revert if called.
*/
function uniswapV3SwapCallback(
int256 /*amount0Delta*/,
int256 /*amount1Delta*/,
bytes calldata /*data*/
) external pure override {
revert("SwapRouterMock: uniswapV3SwapCallback not implemented");
}
}

0 comments on commit a055ca5

Please sign in to comment.