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"); + } +}