From 70d3cb5f6b7c84fdaaf319d5fd1a7b0e1f8b2287 Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Tue, 19 Aug 2025 20:23:45 -0400 Subject: [PATCH] feat(WIP): Uniswap V2 Adapter Hook This commit is a proof of concept for a hook that adapts an existing liquidity source such as Uniswap V2. This allows routers to interact with various liquidity systems using the Uniswap V4 interface and route across Uniswap V4 and others without leaving the lock. --- .gitmodules | 3 + lib/briefcase | 1 + snapshots/UniswapV2AdapterHookForkTest.json | 6 + src/base/hooks/BaseLiquidityAdapterHook.sol | 137 +++++++++++++ src/hooks/UniswapV2AdapterHook.sol | 128 ++++++++++++ src/libraries/UniswapV2Library.sol | 149 ++++++++++++++ test/hooks/UniswapV2AdapterHook.fork.t.sol | 211 ++++++++++++++++++++ 7 files changed, 635 insertions(+) create mode 160000 lib/briefcase create mode 100644 snapshots/UniswapV2AdapterHookForkTest.json create mode 100644 src/base/hooks/BaseLiquidityAdapterHook.sol create mode 100644 src/hooks/UniswapV2AdapterHook.sol create mode 100644 src/libraries/UniswapV2Library.sol create mode 100644 test/hooks/UniswapV2AdapterHook.fork.t.sol diff --git a/.gitmodules b/.gitmodules index 9d6618d5b..1e1479ffa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/permit2"] path = lib/permit2 url = https://github.com/Uniswap/permit2 +[submodule "lib/briefcase"] + path = lib/briefcase + url = https://github.com/uniswap/briefcase 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/snapshots/UniswapV2AdapterHookForkTest.json b/snapshots/UniswapV2AdapterHookForkTest.json new file mode 100644 index 000000000..ccec1d176 --- /dev/null +++ b/snapshots/UniswapV2AdapterHookForkTest.json @@ -0,0 +1,6 @@ +{ + "UniswapV2Adapter_exactInput_usdc": "174728", + "UniswapV2Adapter_exactInput_weth": "169058", + "UniswapV2Adapter_exactOutput_usdc": "174263", + "UniswapV2Adapter_exactOutput_weth": "168539" +} \ No newline at end of file diff --git a/src/base/hooks/BaseLiquidityAdapterHook.sol b/src/base/hooks/BaseLiquidityAdapterHook.sol new file mode 100644 index 000000000..52327c71b --- /dev/null +++ b/src/base/hooks/BaseLiquidityAdapterHook.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +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 {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.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 "../../utils/BaseHook.sol"; +import {DeltaResolver} from "../DeltaResolver.sol"; +import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; + +/// @title BaseLiquidityAdapterHook +/// @author Uniswap V4 Hooks +/// @notice Abstract base contract for adapting external liquidity sources to Uniswap V4 pools +/// @dev Provides core functionality for integrating alternative AMMs and liquidity protocols with V4 +/// @dev Liquidity operations (add/remove) are disabled - all liquidity is managed externally +/// @dev Inheritors must implement: _swapExactInput, _swapExactOutput, and _liquidityExists +abstract contract BaseLiquidityAdapterHook is BaseHook, DeltaResolver { + using CurrencyLibrary for Currency; + using SafeCast for int256; + using SafeCast for uint256; + + /// @notice Thrown when attempting to add or remove liquidity + /// @dev Liquidity operations are disabled as liquidity is managed by the external source + error LiquidityNotAllowed(); + + /// @notice Thrown when initializing a pool that doesn't have corresponding external liquidity + /// @dev The external liquidity source must support the given token pair + error InvalidPool(); + + /// @notice Initializes the base liquidity adapter hook + /// @param _manager The Uniswap V4 pool manager contract + constructor(IPoolManager _manager) BaseHook(_manager) {} + + /// @inheritdoc BaseHook + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: true, + beforeSwap: true, + beforeSwapReturnDelta: true, + beforeAddLiquidity: true, + afterSwap: false, + afterInitialize: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeDonate: false, + afterDonate: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + /// @notice Hook called before pool initialization to validate compatibility + /// @dev Ensures the external liquidity source supports the given token pair + /// @param poolKey The pool configuration containing token pair and fee settings + /// @return bytes4 The beforeInitialize selector on success + function _beforeInitialize(address, PoolKey calldata poolKey, uint160) internal view override returns (bytes4) { + // ensure the pool is supported by the underlying liquidity source + if (!_liquidityExists(poolKey)) revert InvalidPool(); + + return IHooks.beforeInitialize.selector; + } + + /// @notice Hook called before adding liquidity - always reverts + /// @dev Liquidity provision is disabled as all liquidity comes from external source + function _beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata) + internal + pure + override + returns (bytes4) + { + revert LiquidityNotAllowed(); + } + + /// @notice Hook called before swap execution to route through external liquidity + /// @dev Handles both exact input (amountSpecified < 0) and exact output (amountSpecified > 0) + /// @param poolKey The pool configuration + /// @param params Swap parameters including direction, amount, and sqrtPriceLimit + /// @return bytes4 The beforeSwap selector + /// @return swapDelta The token deltas for pool accounting + /// @return uint24 LP fee override (always 0 as fees are handled externally) + function _beforeSwap(address, PoolKey calldata poolKey, SwapParams calldata params, bytes calldata) + internal + override + returns (bytes4, BeforeSwapDelta swapDelta, uint24) + { + bool isExactInput = params.amountSpecified < 0; + + if (isExactInput) { + uint256 amountOut = _swapExactInput(poolKey, params); + swapDelta = toBeforeSwapDelta(-params.amountSpecified.toInt128(), -int128(int256(amountOut))); + } else { + uint256 amountIn = _swapExactOutput(poolKey, params); + swapDelta = toBeforeSwapDelta(-params.amountSpecified.toInt128(), int128(int256(amountIn))); + } + + return (IHooks.beforeSwap.selector, swapDelta, 0); + } + + /// @inheritdoc DeltaResolver + /// @notice Settles positive deltas by transferring tokens to the pool manager + /// @param token The currency to transfer + /// @param amount The amount to transfer to the pool manager + function _pay(Currency token, address, uint256 amount) internal override { + token.transfer(address(poolManager), amount); + } + + /// @notice Executes a swap with exact input amount through external liquidity + /// @param poolKey The pool configuration + /// @param params Swap parameters with negative amountSpecified + /// @return amountOut The amount of output tokens received + function _swapExactInput(PoolKey calldata poolKey, SwapParams calldata params) + internal + virtual + returns (uint256 amountOut); + + /// @notice Executes a swap with exact output amount through external liquidity + /// @param poolKey The pool configuration + /// @param params Swap parameters with positive amountSpecified + /// @return amountIn The amount of input tokens required + function _swapExactOutput(PoolKey calldata poolKey, SwapParams calldata params) + internal + virtual + returns (uint256 amountIn); + + /// @notice Checks if the external liquidity source supports the given pool + /// @param poolKey The pool configuration to validate + /// @return exists True if external liquidity exists for this token pair + function _liquidityExists(PoolKey calldata poolKey) internal view virtual returns (bool exists); +} diff --git a/src/hooks/UniswapV2AdapterHook.sol b/src/hooks/UniswapV2AdapterHook.sol new file mode 100644 index 000000000..46ceac1d6 --- /dev/null +++ b/src/hooks/UniswapV2AdapterHook.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IUniswapV2Factory} from "briefcase/protocols/v2-core/interfaces/IUniswapV2Factory.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IUniswapV2Pair} from "briefcase/protocols/v2-core/interfaces/IUniswapV2Pair.sol"; +import {BaseLiquidityAdapterHook} from "../base/hooks/BaseLiquidityAdapterHook.sol"; +import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {UniswapV2Library} from "../libraries/UniswapV2Library.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; + +/// @title UniswapV2AdapterHook +/// @author Uniswap V4 Hooks +/// @notice Adapter hook that routes V4 swaps through Uniswap V2 liquidity pools +/// @dev Enables V4 pools to leverage existing V2 liquidity without migration +contract UniswapV2AdapterHook is BaseLiquidityAdapterHook { + error InvalidFee(); + error InvalidTickSpacing(); + + uint32 constant POOL_FEE = 3000; + int24 constant POOL_TICK_SPACING = 1; + + /// @notice The Uniswap V2 factory contract for accessing V2 pairs + IUniswapV2Factory public immutable v2Factory; + bytes32 constant UNISWAP_V2_INIT_CODE_HASH = hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"; + + /// @notice Initializes the Uniswap V2 adapter hook + /// @param _manager The Uniswap V4 pool manager contract + /// @param _v2Factory The Uniswap V2 factory contract address + constructor(IPoolManager _manager, IUniswapV2Factory _v2Factory) BaseLiquidityAdapterHook(_manager) { + v2Factory = _v2Factory; + } + + /// @inheritdoc BaseLiquidityAdapterHook + /// @dev Routes exact input swaps through the corresponding V2 pair + function _swapExactInput(PoolKey calldata poolKey, SwapParams calldata params) + internal + override + returns (uint256 amountOut) + { + ( + Currency tokenIn, + Currency tokenOut, + address pair, + uint256 reserveIn, + uint256 reserveOut, + uint256 amountSpecified + ) = _parseSwap(poolKey, params); + amountOut = UniswapV2Library.getAmountOut(amountSpecified, reserveIn, reserveOut); + (uint256 amount0Out, uint256 amount1Out) = params.zeroForOne ? (uint256(0), amountOut) : (amountOut, uint256(0)); + + // Sync output token balance before V2 swap + poolManager.sync(tokenOut); + // Transfer input tokens from V4 pool manager directly to V2 pair + _take(tokenIn, pair, amountSpecified); + IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(poolManager), new bytes(0)); + // Settle with V4 pool manager to account for output tokens received + poolManager.settle(); + } + + /// @inheritdoc BaseLiquidityAdapterHook + /// @dev Routes exact output swaps through the corresponding V2 pair + function _swapExactOutput(PoolKey calldata poolKey, SwapParams calldata params) + internal + override + returns (uint256 amountIn) + { + ( + Currency tokenIn, + Currency tokenOut, + address pair, + uint256 reserveIn, + uint256 reserveOut, + uint256 amountSpecified + ) = _parseSwap(poolKey, params); + amountIn = UniswapV2Library.getAmountIn(amountSpecified, reserveIn, reserveOut); + (uint256 amount0Out, uint256 amount1Out) = + params.zeroForOne ? (uint256(0), amountSpecified) : (amountSpecified, uint256(0)); + + // Sync output token balance before V2 swap + poolManager.sync(tokenOut); + // Transfer input tokens from V4 pool manager directly to V2 pair + _take(tokenIn, pair, amountIn); + IUniswapV2Pair(pair).swap(amount0Out, amount1Out, address(poolManager), new bytes(0)); + // Settle with V4 pool manager to account for output tokens received + poolManager.settle(); + } + + /// @inheritdoc BaseLiquidityAdapterHook + /// @dev Checks if a V2 pair exists for the given token pair + function _liquidityExists(PoolKey calldata poolKey) internal view override returns (bool exists) { + if (poolKey.fee != POOL_FEE) revert InvalidFee(); + if (poolKey.tickSpacing != POOL_TICK_SPACING) revert InvalidTickSpacing(); + return v2Factory.getPair(Currency.unwrap(poolKey.currency0), Currency.unwrap(poolKey.currency1)) != address(0); + } + + /// @notice Parses swap parameters to determine tokens and amounts + /// @dev Extracts token direction, V2 pair address, and swap amount from V4 parameters + /// @param poolKey The V4 pool configuration + /// @param params The V4 swap parameters + /// @return tokenIn The input token currency + /// @return tokenOut The output token currency + /// @return pair The V2 pair contract for this token pair + /// @return reserveIn The reserve of the input token on the pair + /// @return reserveOut The reserve of the output token on the pair + /// @return amountSpecified The absolute swap amount + function _parseSwap(PoolKey calldata poolKey, SwapParams calldata params) + private + view + returns ( + Currency tokenIn, + Currency tokenOut, + address pair, + uint256 reserveIn, + uint256 reserveOut, + uint256 amountSpecified + ) + { + (tokenIn, tokenOut) = + params.zeroForOne ? (poolKey.currency0, poolKey.currency1) : (poolKey.currency1, poolKey.currency0); + (pair, reserveIn, reserveOut) = UniswapV2Library.pairAndReservesFor( + address(v2Factory), UNISWAP_V2_INIT_CODE_HASH, Currency.unwrap(tokenIn), Currency.unwrap(tokenOut) + ); + amountSpecified = + params.amountSpecified > 0 ? uint256(params.amountSpecified) : uint256(-params.amountSpecified); + } +} diff --git a/src/libraries/UniswapV2Library.sol b/src/libraries/UniswapV2Library.sol new file mode 100644 index 000000000..d0be5f586 --- /dev/null +++ b/src/libraries/UniswapV2Library.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.0; + +import {IUniswapV2Pair} from "briefcase/protocols/v2-core/interfaces/IUniswapV2Pair.sol"; + +/// @title Uniswap v2 Helper Library +/// @notice Calculates the recipient address for a command +library UniswapV2Library { + error InvalidReserves(); + error InvalidPath(); + + /// @notice Calculates the v2 address for a pair without making any external calls + /// @param factory The address of the v2 factory + /// @param initCodeHash The hash of the pair initcode + /// @param tokenA One of the tokens in the pair + /// @param tokenB The other token in the pair + /// @return pair The resultant v2 pair address + function pairFor(address factory, bytes32 initCodeHash, address tokenA, address tokenB) + internal + pure + returns (address pair) + { + (address token0, address token1) = sortTokens(tokenA, tokenB); + pair = pairForPreSorted(factory, initCodeHash, token0, token1); + } + + /// @notice Calculates the v2 address for a pair and the pair's token0 + /// @param factory The address of the v2 factory + /// @param initCodeHash The hash of the pair initcode + /// @param tokenA One of the tokens in the pair + /// @param tokenB The other token in the pair + /// @return pair The resultant v2 pair address + /// @return token0 The token considered token0 in this pair + function pairAndToken0For(address factory, bytes32 initCodeHash, address tokenA, address tokenB) + internal + pure + returns (address pair, address token0) + { + address token1; + (token0, token1) = sortTokens(tokenA, tokenB); + pair = pairForPreSorted(factory, initCodeHash, token0, token1); + } + + /// @notice Calculates the v2 address for a pair assuming the input tokens are pre-sorted + /// @param factory The address of the v2 factory + /// @param initCodeHash The hash of the pair initcode + /// @param token0 The pair's token0 + /// @param token1 The pair's token1 + /// @return pair The resultant v2 pair address + function pairForPreSorted(address factory, bytes32 initCodeHash, address token0, address token1) + private + pure + returns (address pair) + { + pair = address( + uint160( + uint256( + keccak256( + abi.encodePacked(hex"ff", factory, keccak256(abi.encodePacked(token0, token1)), initCodeHash) + ) + ) + ) + ); + } + + /// @notice Calculates the v2 address for a pair and fetches the reserves for each token + /// @param factory The address of the v2 factory + /// @param initCodeHash The hash of the pair initcode + /// @param tokenA One of the tokens in the pair + /// @param tokenB The other token in the pair + /// @return pair The resultant v2 pair address + /// @return reserveA The reserves for tokenA + /// @return reserveB The reserves for tokenB + function pairAndReservesFor(address factory, bytes32 initCodeHash, address tokenA, address tokenB) + internal + view + returns (address pair, uint256 reserveA, uint256 reserveB) + { + address token0; + (pair, token0) = pairAndToken0For(factory, initCodeHash, tokenA, tokenB); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pair).getReserves(); + (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + } + + /// @notice Given an input asset amount returns the maximum output amount of the other asset + /// @param amountIn The token input amount + /// @param reserveIn The reserves available of the input token + /// @param reserveOut The reserves available of the output token + /// @return amountOut The output amount of the output token + function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountOut) + { + if (reserveIn == 0 || reserveOut == 0) revert InvalidReserves(); + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = reserveIn * 1000 + amountInWithFee; + amountOut = numerator / denominator; + } + + /// @notice Returns the input amount needed for a desired output amount in a single-hop trade + /// @param amountOut The desired output amount + /// @param reserveIn The reserves available of the input token + /// @param reserveOut The reserves available of the output token + /// @return amountIn The input amount of the input token + function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) + internal + pure + returns (uint256 amountIn) + { + if (reserveIn == 0 || reserveOut == 0) revert InvalidReserves(); + uint256 numerator = reserveIn * amountOut * 1000; + uint256 denominator = (reserveOut - amountOut) * 997; + amountIn = (numerator / denominator) + 1; + } + + /// @notice Returns the input amount needed for a desired output amount in a multi-hop trade + /// @param factory The address of the v2 factory + /// @param initCodeHash The hash of the pair initcode + /// @param amountOut The desired output amount + /// @param path The path of the multi-hop trade + /// @return amount The input amount of the input token + /// @return pair The first pair in the trade + function getAmountInMultihop(address factory, bytes32 initCodeHash, uint256 amountOut, address[] calldata path) + internal + view + returns (uint256 amount, address pair) + { + if (path.length < 2) revert InvalidPath(); + amount = amountOut; + for (uint256 i = path.length - 1; i > 0; i--) { + uint256 reserveIn; + uint256 reserveOut; + + (pair, reserveIn, reserveOut) = pairAndReservesFor(factory, initCodeHash, path[i - 1], path[i]); + amount = getAmountIn(amount, reserveIn, reserveOut); + } + } + + /// @notice Sorts two tokens to return token0 and token1 + /// @param tokenA The first token to sort + /// @param tokenB The other token to sort + /// @return token0 The smaller token by address value + /// @return token1 The larger token by address value + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + } +} diff --git a/test/hooks/UniswapV2AdapterHook.fork.t.sol b/test/hooks/UniswapV2AdapterHook.fork.t.sol new file mode 100644 index 000000000..47037b591 --- /dev/null +++ b/test/hooks/UniswapV2AdapterHook.fork.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {WETH} from "solmate/src/tokens/WETH.sol"; +import {Test, console2} 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 {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import {BaseTokenWrapperHook} from "../../src/base/hooks/BaseTokenWrapperHook.sol"; +import {UniswapV2AdapterHook} from "../../src/hooks/UniswapV2AdapterHook.sol"; +import {IWstETH} from "../../src/interfaces/external/IWstETH.sol"; +import {TestRouter} from "../shared/TestRouter.sol"; +import {IV4Quoter} from "../../src/interfaces/IV4Quoter.sol"; +import {Deploy} from "../shared/Deploy.sol"; + +contract UniswapV2AdapterHookForkTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + // Mainnet addresses + IERC20 constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + WETH constant weth = WETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); + address constant UNISWAP_V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + UniswapV2AdapterHook public hook; + PoolKey poolKey; + PoolKey poolKeySim; + TestRouter public router; + uint160 initSqrtPriceX96; + IV4Quoter quoter; + + // Test user + address alice = makeAddr("alice"); + + bool forked; + + function setUp() public { + try vm.envString("INFURA_API_KEY") returns (string memory) { + console2.log("Forked Ethereum mainnet"); + // Fork mainnet at a specific block for consistency + vm.createSelectFork(vm.rpcUrl("mainnet"), 21_900_000); + + deployFreshManagerAndRouters(); + // replace manager with the real mainnet manager + manager = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90); + router = new TestRouter(manager); + + hook = UniswapV2AdapterHook( + payable( + address( + uint160( + type(uint160).max & clearAllHookPermissionsMask | Hooks.BEFORE_SWAP_FLAG + | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_INITIALIZE_FLAG + ) + ) + ) + ); + deployCodeTo("UniswapV2AdapterHook", abi.encode(manager, UNISWAP_V2_FACTORY), address(hook)); + quoter = Deploy.v4Quoter(address(manager), hex"00"); + + // Create pool key for wstETH/stETH (wstETH has lower address) + poolKey = PoolKey({ + currency0: Currency.wrap(address(usdc)), + currency1: Currency.wrap(address(weth)), + fee: 3000, + tickSpacing: 1, + hooks: IHooks(address(hook)) + }); + + // Initialize pool at current exchange rate + manager.initialize(poolKey, SQRT_PRICE_1_1); + + // Get tokens from whales and set up approvals + deal(address(weth), alice, 100 ether); + deal(address(usdc), alice, 100 ether); + deal(address(weth), address(manager), 100 ether); + deal(address(usdc), address(manager), 100 ether); + + // Approve tokens + vm.startPrank(alice); + weth.approve(address(router), type(uint256).max); + usdc.approve(address(router), type(uint256).max); + vm.stopPrank(); + forked = true; + } catch { + console2.log( + "Skipping forked tests, no infura key found. Add INFURA_API_KEY env var to .env to run forked tests." + ); + } + } + + modifier onlyForked() { + if (forked) { + console2.log("running forked test"); + _; + return; + } + console2.log("skipping forked test"); + } + + function test_fork_swap_exactInput_weth() public onlyForked { + uint256 amountIn = 1 ether; + + vm.startPrank(alice); + uint256 aliceWethBefore = weth.balanceOf(alice); + uint256 aliceUsdcBefore = usdc.balanceOf(alice); + + router.swap( + poolKey, + SwapParams({ + zeroForOne: false, // weth (1) to usdc (0) + amountSpecified: -int256(amountIn), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + "" + ); + vm.snapshotGasLastCall("UniswapV2Adapter_exactInput_weth"); + + vm.stopPrank(); + + uint256 actualAmountOut = usdc.balanceOf(alice) - aliceUsdcBefore; + assertEq(actualAmountOut, 2678967467, "Quoted amount should match the actual amount received"); + assertEq(aliceWethBefore - weth.balanceOf(alice), amountIn, "Incorrect input spent"); + } + + function test_fork_swap_exactInput_usdc() public onlyForked { + uint256 amountIn = 1000_000000; // 1000 USDC + + vm.startPrank(alice); + uint256 aliceWethBefore = weth.balanceOf(alice); + uint256 aliceUsdcBefore = usdc.balanceOf(alice); + + router.swap( + poolKey, + SwapParams({ + zeroForOne: true, // usdc (0) for weth (1) + amountSpecified: -int256(amountIn), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + "" + ); + vm.snapshotGasLastCall("UniswapV2Adapter_exactInput_usdc"); + + vm.stopPrank(); + + uint256 actualAmountOut = weth.balanceOf(alice) - aliceWethBefore; + assertEq(actualAmountOut, 370978046636824314, "Quoted amount should match the actual amount received"); + assertEq(aliceUsdcBefore - usdc.balanceOf(alice), amountIn, "Incorrect input spent"); + } + + function test_fork_swap_exactOutput_weth() public onlyForked { + uint256 amountOut = 1000_000000; // 1000 USDC + + vm.startPrank(alice); + uint256 aliceWethBefore = weth.balanceOf(alice); + uint256 aliceUsdcBefore = usdc.balanceOf(alice); + + router.swap( + poolKey, + SwapParams({ + zeroForOne: false, // weth (1) to usdc (0) + amountSpecified: int256(amountOut), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + "" + ); + vm.snapshotGasLastCall("UniswapV2Adapter_exactOutput_weth"); + + vm.stopPrank(); + + uint256 actualAmountOut = usdc.balanceOf(alice) - aliceUsdcBefore; + assertEq(actualAmountOut, amountOut, "Quoted amount should match the actual amount received"); + assertEq(aliceWethBefore - weth.balanceOf(alice), 373248830169735674, "Incorrect input spent"); + } + + function test_fork_swap_exactOutput_usdc() public onlyForked { + uint256 amountOut = 1 ether; + + vm.startPrank(alice); + uint256 aliceWethBefore = weth.balanceOf(alice); + uint256 aliceUsdcBefore = usdc.balanceOf(alice); + + router.swap( + poolKey, + SwapParams({ + zeroForOne: true, // usdc (0) for weth (1) + amountSpecified: int256(amountOut), + sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 + }), + "" + ); + vm.snapshotGasLastCall("UniswapV2Adapter_exactOutput_usdc"); + + vm.stopPrank(); + + uint256 actualAmountOut = weth.balanceOf(alice) - aliceWethBefore; + assertEq(actualAmountOut, amountOut, "Quoted amount should match the actual amount received"); + assertEq(aliceUsdcBefore - usdc.balanceOf(alice), 2695790431, "Incorrect input spent"); + } +}