-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add UniswapV3Composer unit tests
- Loading branch information
Showing
3 changed files
with
374 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
249 changes: 249 additions & 0 deletions
249
examples/oft-composer-library/test/foundry/UniswapV3Composer.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
123
examples/oft-composer-library/test/mocks/SwapRouterMock.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |