Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/briefcase
Submodule briefcase added at 71de14
6 changes: 6 additions & 0 deletions snapshots/UniswapV2AdapterHookForkTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"UniswapV2Adapter_exactInput_usdc": "174728",
"UniswapV2Adapter_exactInput_weth": "169058",
"UniswapV2Adapter_exactOutput_usdc": "174263",
"UniswapV2Adapter_exactOutput_weth": "168539"
}
137 changes: 137 additions & 0 deletions src/base/hooks/BaseLiquidityAdapterHook.sol
Original file line number Diff line number Diff line change
@@ -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);
}
128 changes: 128 additions & 0 deletions src/hooks/UniswapV2AdapterHook.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading