3分钟Solidity: 15.9 DeFi - Uniswap V4 兑换

59 阅读3分钟

欢迎订阅专栏3分钟Solidity--智能合约--Web3区块链技术必学

Uniswap V4 兑换

Uniswap V4 引入了单一实例 PoolManager,将所有流动性池集中在一个智能合约中管理。

V3版本的主要区别:

  • 单例架构​ - 所有资金池都存在于一个合约中
  • 闪存记账​ - 代币转移仅在最后阶段发生,降低Gas费用
  • 解锁/回调模式​ - 通过回调机制进行交互

要兑换:

  1. 调用带有编码参数的poolManager.unlock()
  2. PoolManager会调用你的unlockCallback()
  3. 在回调函数中:执行交换、结算输入、获取输出
  4. 在解锁完成前,差额必须净值为零
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 以太坊主网上的Uniswap V4 PoolManager
address constant POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

/// @notice 直接在Uniswap V4上使用PoolManager进行交换的示例
/// @dev V4 使用单例PoolManager并采用unlock/unlockCallback模式
contract UniswapV4Swap is IUnlockCallback {
    IPoolManager public immutable poolManager;

    error NotPoolManager();
    error SwapFailed();

    constructor() {
        poolManager = IPoolManager(POOL_MANAGER);
    }

    /// @notice 将输入的精确金额兑换为输出代币
    /// @param key 标识池的池密钥
    /// @param amountIn 输入代币的交换数量
    /// @param minAmountOut 最低可接受输出量
    function swapExactInput(
        PoolKey calldata key,
        uint128 amountIn,
        uint128 minAmountOut
    ) external returns (uint256 amountOut) {
        // Encode swap parameters to pass through unlock callback
        bytes memory data = abi.encode(
            SwapParams({
                key: key,
                amountIn: amountIn,
                minAmountOut: minAmountOut,
                zeroForOne: true,
                sender: msg.sender
            })
        );

        // 发起交换 - PoolManager将调用unlockCallback
        bytes memory result = poolManager.unlock(data);
        amountOut = abi.decode(result, (uint256));
    }

    /// @notice PoolManager解锁后的回调
    /// @dev 这里是实际执行交换逻辑的地方
    function unlockCallback(bytes calldata data)
        external
        override
        returns (bytes memory)
    {
        if (msg.sender != address(poolManager)) revert NotPoolManager();

        SwapParams memory params = abi.decode(data, (SwapParams));

        // 执行交换
        // zeroForOne: true = token0 -> token1, false = token1 -> token0
        // amountSpecified: negative = exact input, positive = exact output
        BalanceDelta delta = poolManager.swap(
            params.key,
            IPoolManager.SwapParams({
                zeroForOne: params.zeroForOne,
                amountSpecified: -int256(uint256(params.amountIn)),
                sqrtPriceLimitX96: params.zeroForOne
                    ? MIN_SQRT_PRICE + 1
                    : MAX_SQRT_PRICE - 1
            }),
            bytes("")
        );

        // 根据差值计算金额
        // delta.amount0() is negative (we owe the pool)
        // delta.amount1() is positive (pool owes us)
        uint256 amountOut = params.zeroForOne
            ? uint256(int256(delta.amount1()))
            : uint256(int256(delta.amount0()));

        if (amountOut < params.minAmountOut) revert SwapFailed();

        // 结算输入代币(支付我们所欠款项)
        Currency inputCurrency = params.zeroForOne
            ? params.key.currency0
            : params.key.currency1;

        IERC20(Currency.unwrap(inputCurrency)).transferFrom(
            params.sender,
            address(poolManager),
            params.amountIn
        );
        poolManager.settle(inputCurrency);

        // Take the output token (receive what we're owed)
        Currency outputCurrency = params.zeroForOne
            ? params.key.currency1
            : params.key.currency0;

        poolManager.take(outputCurrency, params.sender, amountOut);

        return abi.encode(amountOut);
    }

    struct SwapParams {
        PoolKey key;
        uint128 amountIn;
        uint128 minAmountOut;
        bool zeroForOne;
        address sender;
    }
}

// 交换的平方根价格限制
uint160 constant MIN_SQRT_PRICE = 4295128739;
uint160 constant MAX_SQRT_PRICE =
    1461446703485210103287273052203988822378723970342;

// 货币是一个地址包装器(address(0) = 原生ETH)
type Currency is address;

library CurrencyLibrary {
    function unwrap(Currency currency) internal pure returns (address) {
        return Currency.unwrap(currency);
    }
}

using CurrencyLibrary for Currency;

struct PoolKey {
    Currency currency0;
    Currency currency1;
    uint24 fee;
    int24 tickSpacing;
    address hooks;
}

/// @notice 交换操作返回的余额差额
/// @dev 负 = 你欠池子,正 = 池子欠你
type BalanceDelta is int256;

library BalanceDeltaLibrary {
    function amount0(BalanceDelta delta) internal pure returns (int128) {
        return int128(int256(BalanceDelta.unwrap(delta) >> 128));
    }

    function amount1(BalanceDelta delta) internal pure returns (int128) {
        return int128(int256(BalanceDelta.unwrap(delta)));
    }
}

using BalanceDeltaLibrary for BalanceDelta;

interface IPoolManager {
    struct SwapParams {
        bool zeroForOne;
        int256 amountSpecified;
        uint160 sqrtPriceLimitX96;
    }

    function unlock(bytes calldata data) external returns (bytes memory);
    function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData)
        external
        returns (BalanceDelta);
    function settle(Currency currency) external payable returns (uint256);
    function take(Currency currency, address to, uint256 amount) external;
}

interface IUnlockCallback {
    function unlockCallback(bytes calldata data) external returns (bytes memory);
}

interface IERC20 {
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
}

Try on Remix