From 696ffd98eaf94659e95cbe7f289f9274cf2c53f9 Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Sun, 2 Feb 2025 15:21:00 -0500 Subject: [PATCH 1/2] feat: add Uniswap V2 Adapter hook Implements a hook that enables V4 users to access V2 liquidity through V4's swap interface. This adapter creates zero-fee V4 pools that mirror existing V2 pairs, routing swaps through the corresponding V2 pairs while maintaining V2's pricing and slippage behavior. --- .gitmodules | 9 + lib/briefcase | 1 + lib/openzeppelin-contracts | 1 + lib/universal-router | 1 + remappings.txt | 4 + src/hooks/UniswapV2AdapterHook.sol | 230 ++++++++++++++++ test/hooks/UniswapV2AdapterHook.t.sol | 383 ++++++++++++++++++++++++++ 7 files changed, 629 insertions(+) create mode 160000 lib/briefcase create mode 160000 lib/openzeppelin-contracts create mode 160000 lib/universal-router create mode 100644 src/hooks/UniswapV2AdapterHook.sol create mode 100644 test/hooks/UniswapV2AdapterHook.t.sol diff --git a/.gitmodules b/.gitmodules index 9d6618d5b..be9bc5e1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,12 @@ [submodule "lib/permit2"] path = lib/permit2 url = https://github.com/Uniswap/permit2 +[submodule "lib/briefcase"] + path = lib/briefcase + url = https://github.com/uniswap/briefcase +[submodule "lib/universal-router"] + path = lib/universal-router + url = https://github.com/uniswap/universal-router +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/openzeppelin/openzeppelin-contracts diff --git a/lib/briefcase b/lib/briefcase new file mode 160000 index 000000000..71de140e6 --- /dev/null +++ b/lib/briefcase @@ -0,0 +1 @@ +Subproject commit 71de140e687a26294070b601ab4607223ae1e45a diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 000000000..acd4ff74d --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/lib/universal-router b/lib/universal-router new file mode 160000 index 000000000..41183d6eb --- /dev/null +++ b/lib/universal-router @@ -0,0 +1 @@ +Subproject commit 41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd diff --git a/remappings.txt b/remappings.txt index 822fdd158..96fbd5d34 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,9 @@ @uniswap/v4-core/=lib/v4-core/ +@uniswap/briefcase/=lib/briefcase/ +@uniswap/universal-router/=lib/universal-router/ ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/ forge-std/=lib/v4-core/lib/forge-std/src/ openzeppelin-contracts/=lib/v4-core/lib/openzeppelin-contracts/ solmate/=lib/v4-core/lib/solmate/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@uniswap/v2-core/contracts/interfaces/=lib/briefcase/src/protocols/v2-core/interfaces/ diff --git a/src/hooks/UniswapV2AdapterHook.sol b/src/hooks/UniswapV2AdapterHook.sol new file mode 100644 index 000000000..461b9fcce --- /dev/null +++ b/src/hooks/UniswapV2AdapterHook.sol @@ -0,0 +1,230 @@ +pragma solidity ^0.8.0; + +import {IUniswapV2Factory} from "@uniswap/briefcase/src/protocols/v2-core/interfaces/IUniswapV2Factory.sol"; +import {IUniswapV2Pair} from "@uniswap/briefcase/src/protocols/v2-core/interfaces/IUniswapV2Pair.sol"; +import {UniswapV2Library} from "@uniswap/universal-router/contracts/modules/uniswap/v2/UniswapV2Library.sol"; +import { + toBeforeSwapDelta, BeforeSwapDelta, BeforeSwapDeltaLibrary +} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BaseHook} from "../base/hooks/BaseHook.sol"; + +/// @title Uniswap V2 Adapter Hook for V4 +/// @notice Enables V4 users to access V2 liquidity through V4's swap interface +/// @dev This hook creates V4 pools that mirror existing V2 pairs by: +/// 1. Creating V4 pools with 0.3% fee to match V2 pairs +/// 2. Routing V4 swap calls through corresponding V2 pairs +/// 3. Using V2's constant product math for pricing +/// 4. Managing token settlement between V4 and V2 +/// @dev All liquidity remains in V2 pairs - V4 pools are pass-through only +/// @dev Pools can only be created for existing V2 pairs +contract UniswapV2AdapterHook is BaseHook { + using CurrencyLibrary for Currency; + + /// @notice Thrown when attempting to add/remove liquidity through V4 + /// @dev Liquidity operations must go through V2 pairs directly + error LiquidityNotAllowed(); + + /// @notice Thrown when pool fee doesn't match V2's 0.3% fee + /// @dev Fees must match for proper price alignment + error InvalidPoolFee(); + + /// @notice Thrown when tick spacing doesn't match adapter's required spacing + /// @dev Uses fixed tick spacing for V2 compatibility + error InvalidTickSpacing(); + + /// @notice Thrown when V2 pair doesn't exist for token pair + /// @dev V2 pair must be created before V4 adapter pool + error V2PairDoesNotExist(); + + /// @notice Factory contract for looking up V2 pairs + IUniswapV2Factory public immutable v2Factory; + + /// @notice Fee tier matching V2's 0.3% fee (30 bps) + uint24 public constant V2_POOL_FEE = 3000; + + /// @notice Fixed tick spacing for V2 adapter pools + /// @dev Uses minimal spacing since V2 has continuous pricing + int24 public constant V2_TICK_SPACING = 1; + int24 public constant V2_TICK_SPACING = 1; + + /// @notice Creates a new V2 adapter hook + /// @param _manager V4 pool manager contract + /// @param _v2Factory V2 factory for accessing V2 pairs + /// @dev Hook routes V4 swaps through existing V2 pairs + constructor(IPoolManager _manager, IUniswapV2Factory _v2Factory) BaseHook(_manager) { + v2Factory = _v2Factory; + } + + /// @notice Hook permissions required for V2 adapter + /// @dev Enables swap routing and blocks liquidity operations + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: true, + beforeAddLiquidity: true, + beforeRemoveLiquidity: true, + beforeSwap: true, + beforeSwapReturnDelta: true, + afterSwap: false, + afterInitialize: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeDonate: false, + afterDonate: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + /// @inheritdoc IHooks + function beforeInitialize(address, PoolKey calldata poolKey, uint160) external view override returns (bytes4) { + // Check that the pair exists on the v2 factory + if (address(_getPair(poolKey)) == address(0)) revert V2PairDoesNotExist(); + + if (poolKey.fee != V2_POOL_FEE) revert InvalidPoolFee(); + if (poolKey.tickSpacing != V2_TICK_SPACING) revert InvalidTickSpacing(); + + return IHooks.beforeInitialize.selector; + } + + /// @inheritdoc IHooks + function beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) + external + pure + override + returns (bytes4) + { + revert LiquidityNotAllowed(); + } + + /// @inheritdoc IHooks + function beforeRemoveLiquidity( + address, + PoolKey calldata, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external pure override returns (bytes4) { + revert LiquidityNotAllowed(); + } + + /// @inheritdoc IHooks + /// @notice Routes V4 swaps through V2 pairs + /// @dev Swap flow: + /// 1. Finds V2 pair and gets reserves + /// 2. Calculates amounts using V2 math + /// 3. Takes input tokens from sender + /// 4. Executes V2 swap + /// 5. Settles output through pool manager + /// 6. Returns swap delta for V4 accounting + /// @dev Maintains V2's pricing and slippage behavior + function beforeSwap(address, PoolKey calldata poolKey, IPoolManager.SwapParams calldata params, bytes calldata) + external + override + returns (bytes4 selector, BeforeSwapDelta swapDelta, uint24 lpFeeOverride) + { + // Get the corresponding V2 pair and its current reserves + IUniswapV2Pair pair = _getPair(poolKey); + + // Map V4 currencies and reserves to input/output based on swap direction + // zeroForOne: true = token0 to token1, false = token1 to token0 + ( + Currency inputCurrency, // Token being sold + Currency outputCurrency, // Token being bought + uint256 inputAmount, + uint256 amount0Out, + uint256 amount1Out, + int128 amountUnspecified + ) = _getSwapDetails(pair, poolKey, params); + + // Execute the V2 swap: + // Take input tokens from sender and send to V2 pair + poolManager.take(inputCurrency, address(pair), inputAmount); + + // Swap on v2 and settle output tokens on PoolManager + poolManager.sync(outputCurrency); + pair.swap(amount0Out, amount1Out, address(poolManager), new bytes(0)); + poolManager.settle(); + + swapDelta = toBeforeSwapDelta( + // Negate amount specified to cancel to 0 in PoolManager + -int128(params.amountSpecified), + // Amount calculated (positive for input, negative for input needed, positive for output added) + amountUnspecified + ); + + return (IHooks.beforeSwap.selector, swapDelta, 0); // No LP fee since V2 handles fees + } + + /// @notice Helper to get the corresponding V2 pair for a V4 pool + /// @param poolKey The V4 pool key containing the token pair + /// @return The V2 pair contract for these tokens + /// @dev Unwraps V4 Currency types to addresses for V2 compatibility + /// @dev Returns address(0) if pair doesn't exist, which is checked in beforeInitialize + function _getPair(PoolKey memory poolKey) internal view returns (IUniswapV2Pair) { + return IUniswapV2Pair(v2Factory.getPair(Currency.unwrap(poolKey.currency0), Currency.unwrap(poolKey.currency1))); + } + + /// @notice Calculates all necessary swap details for routing through V2 + /// @param pair The V2 pair to execute the swap through + /// @param poolKey The V4 pool key containing swap tokens + /// @param params The V4 swap parameters + /// @return inputCurrency The token being sold + /// @return outputCurrency The token being bought + /// @return inputAmount The amount of input tokens to take from sender + /// @return amount0Out The amount of token0 output from V2 pair + /// @return amount1Out The amount of token1 output from V2 pair + /// @return amountUnspecified The calculated swap amount for V4 delta + /// @dev Handles both exact input and exact output swaps + /// @dev Uses V2's math to calculate amounts and maintain price alignment + function _getSwapDetails(IUniswapV2Pair pair, PoolKey memory poolKey, IPoolManager.SwapParams memory params) + private + view + returns ( + Currency inputCurrency, + Currency outputCurrency, + uint256 inputAmount, + uint256 amount0Out, + uint256 amount1Out, + int128 amountUnspecified + ) + { + // Determine if this is an exact input or exact output swap + bool isExactInput = params.amountSpecified < 0; + (uint256 reserve0, uint256 reserve1,) = pair.getReserves(); + uint256 inputReserve; + uint256 outputReserve; + + // Map V4 currencies and reserves to input/output based on swap direction + // zeroForOne: true = token0 to token1, false = token1 to token0 + ( + inputCurrency, // Token being sold + outputCurrency, // Token being bought + inputReserve, // Reserve of input token in V2 pair + outputReserve // Reserve of output token in V2 pair + ) = params.zeroForOne + ? (poolKey.currency0, poolKey.currency1, reserve0, reserve1) + : (poolKey.currency1, poolKey.currency0, reserve1, reserve0); + + // Calculate input and output amounts using V2 math + uint256 outputAmount; + if (isExactInput) { + inputAmount = uint256(-params.amountSpecified); + outputAmount = UniswapV2Library.getAmountOut(inputAmount, inputReserve, outputReserve); + amountUnspecified = -int128(int256(outputAmount)); + } else { + outputAmount = uint256(params.amountSpecified); + inputAmount = UniswapV2Library.getAmountIn(outputAmount, inputReserve, outputReserve); + amountUnspecified = int128(int256(inputAmount)); + } + + // 2. Prepare V2 swap parameters (amount0Out, amount1Out) + (amount0Out, amount1Out) = params.zeroForOne + ? (uint256(0), outputAmount) // If selling token0, output is token1 + : (outputAmount, uint256(0)); // If selling token1, output is token0 + } +} diff --git a/test/hooks/UniswapV2AdapterHook.t.sol b/test/hooks/UniswapV2AdapterHook.t.sol new file mode 100644 index 000000000..ae43df369 --- /dev/null +++ b/test/hooks/UniswapV2AdapterHook.t.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {UniswapV2FactoryDeployer} from "@uniswap/briefcase/src/deployers/v2-core/UniswapV2FactoryDeployer.sol"; +import {IUniswapV2Factory} from "@uniswap/briefcase/src/protocols/v2-core/interfaces/IUniswapV2Factory.sol"; +import {IUniswapV2Pair} from "@uniswap/briefcase/src/protocols/v2-core/interfaces/IUniswapV2Pair.sol"; + +import {UniswapV2AdapterHook} from "../../src/hooks/UniswapV2AdapterHook.sol"; + +contract UniswapV2AdapterHookTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + UniswapV2AdapterHook public hook; + IUniswapV2Factory public v2Factory; + MockERC20 public token0; + MockERC20 public token1; + PoolKey poolKey; + uint160 initSqrtPriceX96; + + // Users + address alice = makeAddr("alice"); + address bob = makeAddr("bob"); + + function setUp() public { + deployFreshManagerAndRouters(); + + // Deploy mock tokens + token0 = new MockERC20("Token0", "TK0", 18); + token1 = new MockERC20("Token1", "TK1", 18); + vm.label(address(token0), "Token0"); + vm.label(address(token1), "Token1"); + + // Ensure token0 address < token1 address + if (address(token0) > address(token1)) { + (token0, token1) = (token1, token0); + } + + // Deploy V2 factory and create pair + v2Factory = UniswapV2FactoryDeployer.deploy(address(0)); + address pair = v2Factory.createPair(address(token0), address(token1)); + + // Deploy V2 adapter hook + hook = UniswapV2AdapterHook( + address( + uint160( + type(uint160).max & clearAllHookPermissionsMask | Hooks.BEFORE_SWAP_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_INITIALIZE_FLAG + ) + ) + ); + deployCodeTo( + "./foundry-out/UniswapV2AdapterHook.sol/UniswapV2AdapterHook.default.json", + abi.encode(manager, v2Factory), + address(hook) + ); + + // Create pool key for token0/token1 + poolKey = PoolKey({ + currency0: Currency.wrap(address(token0)), + currency1: Currency.wrap(address(token1)), + fee: 3000, // Must match V2's 0.3% fee + tickSpacing: hook.V2_TICK_SPACING(), + hooks: IHooks(address(hook)) + }); + + // Initialize V4 pool + initSqrtPriceX96 = uint160(TickMath.getSqrtPriceAtTick(0)); + manager.initialize(poolKey, initSqrtPriceX96); + + // Add liquidity to V2 pair + token0.mint(pair, 100 ether); + token1.mint(pair, 100 ether); + vm.startPrank(alice); + IUniswapV2Pair(pair).mint(alice); + vm.stopPrank(); + + _addUnrelatedLiquidity(); + } + + function test_initialization() public view { + assertEq(address(hook.v2Factory()), address(v2Factory)); + assertEq(hook.V2_POOL_FEE(), 3000); + } + + function test_swap_exactInput_zeroForOne() public { + uint256 swapAmount = 1 ether; + uint256 expectedOutput = _getV2AmountOut(swapAmount, address(token0), address(token1)); + + vm.startPrank(alice); + token0.mint(alice, swapAmount); + token0.approve(address(swapRouter), type(uint256).max); + + uint256 aliceToken0Before = token0.balanceOf(alice); + uint256 aliceToken1Before = token1.balanceOf(alice); + uint256 managerToken0Before = token0.balanceOf(address(manager)); + uint256 managerToken1Before = token1.balanceOf(address(manager)); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + swapRouter.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: -int256(swapAmount), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + testSettings, + "" + ); + + vm.stopPrank(); + + assertEq(aliceToken0Before - token0.balanceOf(alice), swapAmount); + assertEq(token1.balanceOf(alice) - aliceToken1Before, expectedOutput); + assertEq(managerToken0Before, token0.balanceOf(address(manager))); + assertEq(managerToken1Before, token1.balanceOf(address(manager))); + } + + function test_swap_exactInput_oneForZero() public { + uint256 swapAmount = 1 ether; + uint256 expectedOutput = _getV2AmountOut(swapAmount, address(token1), address(token0)); + + vm.startPrank(alice); + token1.mint(alice, swapAmount); + token1.approve(address(swapRouter), type(uint256).max); + + uint256 aliceToken0Before = token0.balanceOf(alice); + uint256 aliceToken1Before = token1.balanceOf(alice); + uint256 managerToken0Before = token0.balanceOf(address(manager)); + uint256 managerToken1Before = token1.balanceOf(address(manager)); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + swapRouter.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: false, + amountSpecified: -int256(swapAmount), + sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1 + }), + testSettings, + "" + ); + + vm.stopPrank(); + + assertEq(aliceToken1Before - token1.balanceOf(alice), swapAmount); + assertEq(token0.balanceOf(alice) - aliceToken0Before, expectedOutput); + assertEq(managerToken0Before, token0.balanceOf(address(manager))); + assertEq(managerToken1Before, token1.balanceOf(address(manager))); + } + + function test_swap_exactOutput_zeroForOne() public { + uint256 outputAmount = 1 ether; + uint256 expectedInput = _getV2AmountIn(outputAmount, address(token0), address(token1)); + + vm.startPrank(alice); + token0.mint(alice, 100 ether); + token0.approve(address(swapRouter), type(uint256).max); + + uint256 aliceToken0Before = token0.balanceOf(alice); + uint256 aliceToken1Before = token1.balanceOf(alice); + uint256 managerToken0Before = token0.balanceOf(address(manager)); + uint256 managerToken1Before = token1.balanceOf(address(manager)); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + swapRouter.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: int256(outputAmount), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + testSettings, + "" + ); + + vm.stopPrank(); + + assertEq(aliceToken0Before - token0.balanceOf(alice), expectedInput); + assertEq(token1.balanceOf(alice) - aliceToken1Before, outputAmount); + assertEq(managerToken0Before, token0.balanceOf(address(manager))); + assertEq(managerToken1Before, token1.balanceOf(address(manager))); + } + + function test_swap_exactOutput_oneForZero() public { + uint256 outputAmount = 1 ether; + uint256 expectedInput = _getV2AmountIn(outputAmount, address(token1), address(token0)); + + vm.startPrank(alice); + token1.mint(alice, 100 ether); + token1.approve(address(swapRouter), type(uint256).max); + + uint256 aliceToken0Before = token0.balanceOf(alice); + uint256 aliceToken1Before = token1.balanceOf(alice); + uint256 managerToken0Before = token0.balanceOf(address(manager)); + uint256 managerToken1Before = token1.balanceOf(address(manager)); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + swapRouter.swap( + poolKey, + IPoolManager.SwapParams({ + zeroForOne: false, + amountSpecified: int256(outputAmount), + sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1 + }), + testSettings, + "" + ); + + vm.stopPrank(); + + assertEq(aliceToken1Before - token1.balanceOf(alice), expectedInput); + assertEq(token0.balanceOf(alice) - aliceToken0Before, outputAmount); + assertEq(managerToken0Before, token0.balanceOf(address(manager))); + assertEq(managerToken1Before, token1.balanceOf(address(manager))); + } + + function test_revertAddLiquidity() public { + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(hook), + IHooks.beforeAddLiquidity.selector, + abi.encodeWithSelector(UniswapV2AdapterHook.LiquidityNotAllowed.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + + modifyLiquidityRouter.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000e18, + salt: bytes32(0) + }), + "" + ); + } + + function test_revertRemoveLiquidity() public { + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(hook), + IHooks.beforeRemoveLiquidity.selector, + abi.encodeWithSelector(UniswapV2AdapterHook.LiquidityNotAllowed.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + + modifyLiquidityRouter.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: -1000e18, + salt: bytes32(0) + }), + "" + ); + } + + function test_revertInvalidPoolInitialization() public { + // Try to initialize with wrong fee + PoolKey memory invalidKey = PoolKey({ + currency0: Currency.wrap(address(token0)), + currency1: Currency.wrap(address(token1)), + fee: 100, // Invalid: must be 3000 to match V2 + tickSpacing: 60, + hooks: IHooks(address(hook)) + }); + + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(hook), + IHooks.beforeInitialize.selector, + abi.encodeWithSelector(UniswapV2AdapterHook.InvalidPoolFee.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + manager.initialize(invalidKey, initSqrtPriceX96); + + // Try to initialize without V2 pair + MockERC20 randomToken = new MockERC20("Random", "RND", 18); + invalidKey = PoolKey({ + currency0: Currency.wrap(address(token0)), + currency1: Currency.wrap(address(randomToken)), + fee: 3000, + tickSpacing: 60, + hooks: IHooks(address(hook)) + }); + + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(hook), + IHooks.beforeInitialize.selector, + abi.encodeWithSelector(UniswapV2AdapterHook.V2PairDoesNotExist.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + manager.initialize(invalidKey, initSqrtPriceX96); + } + + // Helper to calculate V2 output amount + function _getV2AmountOut(uint256 amountIn, address tokenIn, address tokenOut) + internal + view + returns (uint256 amountOut) + { + IUniswapV2Pair pair = IUniswapV2Pair(v2Factory.getPair(tokenIn, tokenOut)); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + (uint112 reserveIn, uint112 reserveOut) = tokenIn < tokenOut ? (reserve0, reserve1) : (reserve1, reserve0); + + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = reserveIn * 1000 + amountInWithFee; + amountOut = numerator / denominator; + } + + // Helper to calculate V2 input amount + function _getV2AmountIn(uint256 amountOut, address tokenIn, address tokenOut) + internal + view + returns (uint256 amountIn) + { + IUniswapV2Pair pair = IUniswapV2Pair(v2Factory.getPair(tokenIn, tokenOut)); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + (uint112 reserveIn, uint112 reserveOut) = tokenIn < tokenOut ? (reserve0, reserve1) : (reserve1, reserve0); + + uint256 numerator = reserveIn * amountOut * 1000; + uint256 denominator = (reserveOut - amountOut) * 997; + amountIn = (numerator / denominator) + 1; + } + + function _addUnrelatedLiquidity() internal { + // Create a hookless pool key for ETH/WETH + PoolKey memory unrelatedPoolKey = PoolKey({ + currency0: Currency.wrap(address(token0)), + currency1: Currency.wrap(address(token1)), + fee: 100, + tickSpacing: 60, + hooks: IHooks(address(0)) + }); + + manager.initialize(unrelatedPoolKey, uint160(TickMath.getSqrtPriceAtTick(0))); + + token0.mint(address(this), 100 ether); + token1.mint(address(this), 100 ether); + token0.approve(address(modifyLiquidityRouter), type(uint256).max); + token1.approve(address(modifyLiquidityRouter), type(uint256).max); + modifyLiquidityRouter.modifyLiquidity{value: 100 ether}( + unrelatedPoolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: -120, + tickUpper: 120, + liquidityDelta: 1000e18, + salt: bytes32(0) + }), + "" + ); + } +} From 879d1ab562bc280e5cfecab6812070da3646f52d Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Sun, 2 Feb 2025 15:40:46 -0500 Subject: [PATCH 2/2] fix: compiler issues --- src/hooks/UniswapV2AdapterHook.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/UniswapV2AdapterHook.sol b/src/hooks/UniswapV2AdapterHook.sol index 461b9fcce..1ae4da0de 100644 --- a/src/hooks/UniswapV2AdapterHook.sol +++ b/src/hooks/UniswapV2AdapterHook.sol @@ -47,10 +47,9 @@ contract UniswapV2AdapterHook is BaseHook { /// @notice Fee tier matching V2's 0.3% fee (30 bps) uint24 public constant V2_POOL_FEE = 3000; - /// @notice Fixed tick spacing for V2 adapter pools + /// @notice Fixed tick spacing sentinel for V2 adapter pools /// @dev Uses minimal spacing since V2 has continuous pricing int24 public constant V2_TICK_SPACING = 1; - int24 public constant V2_TICK_SPACING = 1; /// @notice Creates a new V2 adapter hook /// @param _manager V4 pool manager contract