Uniswap V4 兑换功能解析:深入闪电会计与执行代码

  • cyfrin
  • 发布于 2025-03-26 22:53
  • 阅读 34

本文深入探讨了Uniswap V4的兑换机制,特别关注了引入的闪电会计模型及其在交换执行中的应用。文章通过详细的代码示例和逐步分析,展示了交易的执行流程、会计管理及状态控制等关键概念,适合希望深入了解Uniswap V4的开发者和技术爱好者。

深入探讨 Uniswap V4 的兑换机制,详细了解闪电会计(flash accounting)、瞬时存储和执行流程,通过详细的代码示例和分析进行说明。

引言

我一直在探索 Uniswap V4,以更好地理解其兑换执行。为了加深我的理解,我分析了驱动兑换的代码,专注于在 V4 中引入的新“闪电会计”模型:如何使用瞬时存储创建、结算和跟踪债务,以及何时实际移动价值。如果你对 Uniswap V4 兑换的内部工作原理感到好奇,特别是从会计的角度来看,这个指南就是为你准备的。让我们深入探讨吧!

0. 兑换设置

为了演示这一点,我们将使用一个 ERC-20 到 ERC-20 的兑换,具体是 WBTC 到 USDC( pool)在以太坊主网的本地分叉上:

这是找到的测试:

function testSwapWBTCForUSDC() public {
    uint128 amountIn = 1e7;
    uint128 minAmountOut = 0;
    deal(WBTC_ADDRESS, address(this), amountIn);

    PoolKey memory wbtc_usdc_key = PoolKey({
        currency0: Currency.wrap(WBTC_ADDRESS),
        currency1: Currency.wrap(USDC_ADDRESS),
        fee: 3000,
        tickSpacing: 60,
        hooks: IHooks(address(0))
    });

    WBTC.approve(PERMIT2_ADDRESS, amountIn);
    PERMIT2.approve(WBTC_ADDRESS, UNIVERSAL_ROUTER_ADDRESS, amountIn, uint48(block.timestamp));

    bytes memory actions = abi.encodePacked(
        uint8(Actions.SWAP_EXACT_IN_SINGLE),
        uint8(Actions.SETTLE_ALL),
        uint8(Actions.TAKE_ALL)
    );

    bytes[] memory params = new bytes[](3);
    params[0] = abi.encode( // SWAP_EXACT_IN_SINGLE
        IV4Router.ExactInputSingleParams({
            poolKey: wbtc_usdc_key,
            zeroForOne: true,
            amountIn: amountIn,
            amountOutMinimum: minAmountOut,
            hookData: bytes("")
        })
    );
    params[1] = abi.encode(wbtc_usdc_key.currency0, amountIn); // SETTLE_ALL
    params[2] = abi.encode(wbtc_usdc_key.currency1, minAmountOut); // TAKE_ALL

    bytes[] memory inputs = new bytes[](1);
    inputs[0] = abi.encode(actions, params);

    bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
    UNIVERSAL_ROUTER.execute(commands, inputs, block.timestamp);

    assertGt(USDC.balanceOf(address(this)), minAmountOut);
}

完整的测试套件,包括与原生 ETH 的兑换,可以在 这里 找到。你还可以找到一个 图表,作为你阅读时的便捷参考。

1. UniversalRouter + Dispatcher

兑换执行从我们测试中的以下几行开始:

bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
UNIVERSAL_ROUTER.execute(commands, inputs, block.timestamp);

UniversalRouter::execute 是第一步:

/// @inheritdoc Dispatcher
function execute(bytes calldata commands, bytes[] calldata inputs) public payable override isNotLocked {
    bool success;
    bytes memory output;
    uint256 numCommands = commands.length;
    if (inputs.length != numCommands) revert LengthMismatch();

    // loop through all given commands, execute them and pass along outputs as defined
    for (uint256 commandIndex = 0; commandIndex < numCommands; commandIndex++) {
        bytes1 command = commands[commandIndex];

        bytes calldata input = inputs[commandIndex];

        (success, output) = dispatch(command, input); // <--- pass to dispatch

        if (!success && successRequired(command)) {
            revert ExecutionFailed({commandIndex: commandIndex, message: output});
        }
    }
}

由于 Commands.V4_SWAP 是我们测试中的唯一命令,循环仅运行一次。

接下来,执行移至命令分发函数 Dispatcher::dispatch

/// @notice Decodes and executes the given command with the given inputs
/// @param commandType The command type to execute
/// @param inputs The inputs to execute the command with
/// @dev 2 masks are used to enable use of a nested-if statement in execution for efficiency reasons
/// @return success True on success of the command, false on failure
/// @return output The outputs or error messages, if any, from the command
function dispatch(bytes1 commandType, bytes calldata inputs) internal returns (bool success, bytes memory output) {
    uint256 command = uint8(commandType & Commands.COMMAND_TYPE_MASK);

    success = true;

    // 0x00 <= command < 0x21
    if (command < Commands.EXECUTE_SUB_PLAN) {
        // 0x00 <= command < 0x10
        if (command < Commands.V4_SWAP) {
            ...
        } else {
            // 0x10 <= command < 0x21
            if (command == Commands.V4_SWAP) {
                // pass the calldata provided to V4SwapRouter._executeActions (defined in BaseActionsRouter)
                _executeActions(inputs);
                // This contract MUST be approved to spend the token since its going to be doing the call on the position manager
            } else if {
                ...
        }
    } else {
        ...
    }
}

这些函数的目的是在不同的 Uniswap 版本之间路由调用。由于我们使用的是 V4,我们将进入 Uniswap V4 peripheral。

2. BaseActionsRouter

执行流程到达 BaseActionsRouter::_executeActions,其中出现了 Uniswap V4 的第一个 关键新颖之处

/// @notice internal function that triggers the execution of a set of actions on v4
/// @dev inheriting contracts should call this function to trigger execution
function _executeActions(bytes calldata unlockData) internal {
    poolManager.unlock(unlockData);
}

这个功能标志着与实际池的第一次直接交互,引入了 Uniswap V4 的一个重大变化:PoolManager。作为一个 单例合约PoolManager 持有所有 Uniswap V4 池,需在任何交互发生之前解锁。PoolManager::unlock 可以说是它最重要的功能,因为它确保了正确的会计,我们稍后会探讨这个主题。

让我们详细介绍 PoolManager::unlock

/// @inheritdoc IPoolManager
function unlock(bytes calldata data) external override returns (bytes memory result) {
    if (Lock.isUnlocked()) AlreadyUnlocked.selector.revertWith();

    Lock.unlock();

    // the caller does everything in this callback, including paying what they owe via calls to settle
    result = IUnlockCallback(msg.sender).unlockCallback(data);

    // ... verifying important accounting
}

需要注意的关键行:

Lock.unlock();

    result = IUnlockCallback(msg.sender).unlockCallback(data);

首先,合约的 状态 被设为 unlocked,然后执行返回到 msg.senderUniversalRouter)。这在 Uniswap V4 中引入了执行流程中的一个关键变化:

  1. 通过 unlock 解锁 PoolManager

  2. PoolManager 将执行返还给 msg.sender

  3. msg.sender 与 Uniswap V4 进行交互(例如,执行兑换/添加/移除流动性)

  4. 会计被验证为正确

  5. PoolManager 再次被锁定

在我们的例子中,msg.senderBaseActionsRouter,它是 UniversalRouter 的一部分。

兑换的执行在 SafeCallback::unlockCallback 中继续:

/// @inheritdoc IUnlockCallback
/// @dev We force the onlyPoolManager modifier by exposing a virtual function after the onlyPoolManager check.
function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
    return _unlockCallback(data);
}

这个函数简单地将调用转发给 SafeCallback::_unlockCallback,该函数被 BaseActionsRouter::_unlockCallback 重写:

/// @notice function that is called by the PoolManager through the SafeCallback.unlockCallback
/// @param data Abi encoding of (bytes actions, bytes[] params)
/// where params[i] is the encoded parameters for actions[i]
function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
    // abi.decode(data, (bytes, bytes[]));
    (bytes calldata actions, bytes[] calldata params) = data.decodeActionsRouterParams();
    _executeActionsWithoutUnlock(actions, params);
    return "";
}

此时,PoolManager 已解锁,执行继续在 BaseActionsRouter::_executeActionsWithoutUnlock 中:

function _executeActionsWithoutUnlock(bytes calldata actions, bytes[] calldata params) internal {
    uint256 numActions = actions.length;
    if (numActions != params.length) revert InputLengthMismatch();

    for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) {
        uint256 action = uint8(actions[actionIndex]);

        _handleAction(action, params[actionIndex]);
    }
}

这个函数循环遍历并依次执行每个动作。

现在是时候重新审视在我们的兑换的 foundry 测试中传入的动作:

bytes memory actions = abi.encodePacked(
    uint8(Actions.SWAP_EXACT_IN_SINGLE),
    uint8(Actions.SETTLE_ALL),
    uint8(Actions.TAKE_ALL)
);

会计如下:

  1. SWAP_EXACT_IN_SINGLE:创建从调用者到池的输入代币( WBTC)的债务,以及从池到调用者的输出代币( USDC)的债务。

  2. SETTLE_ALL:结算从调用者到池的债务。

  3. TAKE_ALL:从池收集应得的输出代币并发送给调用者。

现在,让我们探讨每个动作。

3. SWAP_EXACT_IN_SINGLE

3.1 V4Router

V4Router::_handleAction 处理不同动作。在我们的案例中,我们对处理 Actions.SWAP_EXACT_IN_SINGLE 感兴趣:

function _handleAction(uint256 action, bytes calldata params) internal override {
    // swap actions and payment actions in different blocks for gas efficiency
    if (action < Actions.SETTLE) {
            ...
        } else if (action == Actions.SWAP_EXACT_IN_SINGLE) {
            IV4Router.ExactInputSingleParams calldata swapParams = params.decodeSwapExactInSingleParams();
            _swapExactInputSingle(swapParams);
            return;
        } else if {
            ...
    } else {
        ...
    }
    revert UnsupportedAction(action);
}

在这里,swapParams 被解码并传递到 V4Router::_swapExactInputSingle

function _swapExactInputSingle(IV4Router.ExactInputSingleParams calldata params) private {
    uint128 amountIn = params.amountIn;
    if (amountIn == ActionConstants.OPEN_DELTA) {
        amountIn =
            _getFullCredit(params.zeroForOne ? params.poolKey.currency0 : params.poolKey.currency1).toUint128();
    }
    uint128 amountOut =
        _swap(params.poolKey, params.zeroForOne, -int256(uint256(amountIn)), params.hookData).toUint128();
    if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);
}

第一个条件 if (amountIn == ActionConstants.OPEN_DELTA) 检查是否使用 PoolManager 中的现有“标签”进行兑换。由于我们从 0 开始,我们可以忽略这个条件,它仅适用于进行中间兑换,并且超出了该测试的范围。

然后执行转向 V4Router::_swap

function _swap(PoolKey memory poolKey, bool zeroForOne, int256 amountSpecified, bytes calldata hookData)
    private
    returns (int128 reciprocalAmount)
{
    // for protection of exactOut swaps, sqrtPriceLimit is not exposed as a feature in this contract
    unchecked {
        BalanceDelta delta = poolManager.swap(
            poolKey,
            IPoolManager.SwapParams(
                zeroForOne, amountSpecified, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1
            ),
            hookData
        );

        reciprocalAmount = (zeroForOne == amountSpecified < 0) ? delta.amount1() : delta.amount0();
    }
}

该函数通过调用 PoolManager 执行实际的兑换。

3.2 PoolManager

兑换本身发生在 PoolManager::swap

/// @inheritdoc IPoolManager
function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)
    external
    onlyWhenUnlocked
    noDelegateCall
    returns (BalanceDelta swapDelta)
{
    if (params.amountSpecified == 0) SwapAmountCannotBeZero.selector.revertWith();
    PoolId id = key.toId();
    Pool.State storage pool = _getPool(id);
    pool.checkPoolInitialized();

    BeforeSwapDelta beforeSwapDelta;
    {
        int256 amountToSwap;
        uint24 lpFeeOverride;
        (amountToSwap, beforeSwapDelta, lpFeeOverride) = key.hooks.beforeSwap(key, params, hookData);

        // execute swap, account protocol fees, and emit swap event
        // _swap is needed to avoid stack too deep error
        swapDelta = _swap(
            // ... swap
        );
    }

    BalanceDelta hookDelta;
    (swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta);

    // if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0
    if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks));

    _accountPoolBalanceDelta(key, swapDelta, msg.sender);
}

该函数:

  1. 检索池状态。

  2. 执行任何预兑换Hook,在我们的案例中由于我们的池没有Hook,因此不适用。

  3. 执行兑换。

  4. 应用后兑换Hook,同样,在我们的案例中不适用。

  5. 记录余额变化。

兑换结果为 BalanceDelta(压缩数据类型,表示两个 int128 值打包到单个 int256 中),记录了 UniversalRouterPoolManager 之间的债务。

兑换完成后,此余额 delta 被存储:

_accountPoolBalanceDelta(key, swapDelta, msg.sender);

PoolManager::_accountPoolBalanceDelta 使用瞬时存储记录余额 delta,这是以太坊虚拟机的一个新特性( EVM),它仅持续到交易结束:

/// @notice Accounts the deltas of 2 currencies to a target address
function _accountPoolBalanceDelta(PoolKey memory key, BalanceDelta delta, address target) internal {
    _accountDelta(key.currency0, delta.amount0(), target);
    _accountDelta(key.currency1, delta.amount1(), target);
}

PoolManager::_accountDelta 做了两件重要的事情:

/// @notice Adds a balance delta in a currency for a target address
function _accountDelta(Currency currency, int128 delta, address target) internal {
    if (delta == 0) return;

    (int256 previous, int256 next) = currency.applyDelta(target, delta);

    if (next == 0) {
        NonzeroDeltaCount.decrement();
    } else if (previous == 0) {
        NonzeroDeltaCount.increment();
    }
}
  • currency.applyDelta(target, delta) 记录未偿还的余额。

  • NonzeroDeltaCount 跟踪未偿还债务的数量。

这是至关重要的,因为它决定了我们是否可以退出 PoolManager::unlock,为此,NonzeroDeltaCount 必须为 0。

在兑换后:

  • UniversalRouter(因此我们)欠 PoolManagerWBTC

  • PoolManagerUniversalRouterUSDC

  • NonzeroDeltaCount 增加至 2(每个方向一笔债务)。

兑换完成后,swapDelta 被返回。

在我们的测试中,值为:-3402823669209384634633746074317682105591720676。由于 BalanceDelta 将两个值打包在一起,让我们来解码它:

function testSplitInt() public {
    BalanceDelta delta = BalanceDelta.wrap(-3402823669209384634633746074317682105591720676);
    console.logInt(delta.amount0()); // -10_000_000
    console.logInt(delta.amount1()); // 8_968_279_324
}

这证实:

  • PoolManager 欠 10_000_000 (-1e7) WBTC

  • PoolManager 欠 8_968_279_324 (~9_000e6) USDC

3.3 V4Router

V4Router::_swapExactInputSingle 的最后一步是滑点检查:

if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);

我们在测试中将 params.amountOutMinimum 设为 0,因此此检查通过。然而,在实际兑换中永远不应这样做,因为这会使你的兑换易受滑点滥用的影响。

至此,SWAP_EXACT_IN_SINGLE 完全执行!

下一步是清理债务,以便我们可以退出 PoolManager::unlock 并完成兑换。

4. SETTLE_ALL

4.1 V4Router

下一个动作是 Actions.SETTLE_ALL,通过 V4SwapRouter::_handleAction 路由:

if (action == Actions.SETTLE_ALL) {
    (Currency currency, uint256 maxAmount) = params.decodeCurrencyAndUint256();
    uint256 amount = _getFullDebt(currency);
    if (amount > maxAmount) revert V4TooMuchRequested(maxAmount, amount);
    _settle(currency, msgSender(), amount);
    return;
}

在我们的测试中,我们传送的参数是:

params[1] = abi.encode(WBTC_USDC_KEY.currency0, amountIn);

在这里:

  • currency0(即 currency)是 WBTC

  • amountIn(即 maxAmount)是 1e7,代表我们欠 PoolManagerWBTC 数量。

DeltaResolver::_getFullDebt 查询 PoolManager 以确定实际债务:

function _getFullDebt(Currency currency) internal view returns (uint256 amount) {
    int256 _amount = poolManager.currencyDelta(address(this), currency);
    // 如果金额为正,则应该是收取而不是结算。
    if (_amount > 0) revert DeltaNotNegative(currency);
    // 由于池的总供应量有限,这种转换是安全的
    amount = uint256(-_amount);
}

该函数调用 PoolManager::exttload,返回我们的债务值。

查看 Forge 的事务跟踪( -vvvv),我们看到:

├─ [859] POOL_MANAGER::exttload(0xcc542c39d285d4bff2e6d92da545b4deeab7b8d383577645f35f8576aa18a8a8) [staticcall]
│   └─ ← [Return] 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff676980

这个结果( 0xffff...676980)对应于 -1e7,确认我们欠 10_000_000 WBTC

接下来执行转向 DeltaResolver::_settle:

function _settle(Currency currency, address payer, uint256 amount) internal {
    if (amount == 0) return;

    poolManager.sync(currency);
    if (currency.isAddressZero()) {
        poolManager.settle{value: amount}();
    } else {
        _pay(currency, payer, amount);
        poolManager.settle();
    }
}

第一步是调用 poolManager.sync(currency)

在结算债务之前,我们必须同步 PoolManager 中的瞬时存储,以确保其正确跟踪即将到来的余额。

4.2 PoolManager

PoolManager::sync

function sync(Currency currency) external {
    // address(0) 用于原生货币
    if (currency.isAddressZero()) {
        // 保留的余额未被用于原生结算,因此我们只需重置货币。
        CurrencyReserves.resetCurrency();
    } else {
        uint256 balance = currency.balanceOfSelf();
        CurrencyReserves.syncCurrencyAndReserves(currency, balance);
    }
}

CurrencyReserves::syncCurrencyAndReserves 将当前的 WTBC 余额写入瞬时存储:

function syncCurrencyAndReserves(Currency currency, uint256 value) internal {
    assembly ("memory-safe") {
        tstore(CURRENCY_SLOT, and(currency, 0xffffffffffffffffffffffffffffffffffffffffffff))
        tstore(RESERVES_OF_SLOT, value)
    }
}

这确保 PoolManger 在结算之前正确跟踪 WBTC 余额。

4.3 V4SwapRouter

因为我们不是用原生 ETH 结算,所以执行继续到 V4SwapRouter::_pay。这是 UniversalRouter 库的一部分,具体是 V4SwapRouter::_pay

function _pay(Currency token, address payer, uint256 amount) internal override {
    payOrPermit2Transfer(Currency.unwrap(token), payer, address(poolManager), amount);
}

这导致 Permit2Payments::payOrPermit2Transfer

function payOrPermit2Transfer(address token, address payer, address recipient, uint256 amount) internal {
    if (payer == address(this)) pay(token, recipient, amount);
    else permit2TransferFrom(token, payer, recipient, amount.toUint160());
}

因为 payer 不是 address(this),所以执行继续在 Permit2Payments::permit2TransferFrom

function permit2TransferFrom(address token, address from, address to, uint160 amount) internal {
    PERMIT2.transferFrom(from, to, amount, token);
}

这调用 PERMIT2,最终执行 WBTC 转移:

├─ [9162] PERMIT2::transferFrom(UniV4Swap: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], POOL_MANAGER: [0x000000000004444c5dc75cB358380D2e3dE08A90], 10000000 [1e7], WBTC: [0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599])
│   ├─ [7770] WBTC::transferFrom(UniV4Swap: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], POOL_MANAGER: [0x000000000004444c5dc75cB358380D2e3dE08A90], 10000000 [1e7])
│   │   ├─ emit Transfer(from: UniV4Swap: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: POOL_MANAGER: [0x000000000004444c5dc75cB358380D2e3dE08A90], value: 10000000 [1e7])
│   │   └─ ← [Return] true
│   └─ ← [Return]

由于 UniversalRouter 执行移转,用户必须通过 Permit2 批准 UniversalRouter 转移 WBTC。这在我们的测试中完成:

PERMIT2.approve(WBTC_ADDRESS, UNIVERSAL_ROUTER_ADDRESS, amountIn, uint48(block.timestamp));

我们已经将债务代币转账给池。现在是时候结算会计了。

4.4 V4Router

回到 DeltaResolver::_settle,执行现在进入 poolManager.settle()

4.5 PoolManager

PoolManager::settlePoolManager::_settle 处理最终结算:

function settle() external payable onlyWhenUnlocked returns (uint256) {
    return _settle(msg.sender);
}

...

// if settling native, integrators should still call `sync` first to avoid DoS attack vectors
function _settle(address recipient) internal returns (uint256 paid) {
    Currency currency = CurrencyReserves.getSyncedCurrency();

    // if not previously synced, or the syncedCurrency slot has been reset, expects native currency to be settled
    if (currency.isAddressZero()) {
        paid = msg.value;
    } else {
        if (msg.value > 0) NonzeroNativeValue.selector.revertWith();
        // Reserves are guaranteed to be set because currency and reserves are always set together
        uint256 reservesBefore = CurrencyReserves.getSyncedReserves();
        uint256 reservesNow = currency.balanceOfSelf();
        paid = reservesNow - reservesBefore;
        CurrencyReserves.resetCurrency();
    }

    _accountDelta(currency, paid.toInt128(), recipient);
}

因为我们不使用 ETH,这个函数计算结算数额并更新瞬时存储。此时之前的 sync 调用变得至关重要。它确保 CurrencyReserves.getSyncedCurrency() 返回 WBTC,并且 CurrencyReserves.getSyncedReserves() 反映转移前的余额。

因此,reservesNow - reservesBefore 正确反映了结算的金额,该值存储在 paid 中。然后,CurrencyReserves.resetCurrency() 清除瞬时存储,防止残留的货币数据影响未来的操作。

最后,_accountDelta(currency, paid.toInt128(), recipient) 取消了我们的债务:

function _accountDelta(Currency currency, int128 delta, address target) internal {
    if (delta == 0) return;

    (int256 previous, int256 next) = currency.applyDelta(target, delta);

    if (next == 0) {
        NonzeroDeltaCount.decrement();
    } else if (previous == 0) {
        NonzeroDeltaCount.increment();
    }
}

此时,我们的 WBTC 债务已被清除。接下来,我们声明 PoolManager 欠我们的 USDC

注意:如果我们没有调用 sync,CurrencyReserves.getSyncedCurrency() 函数将返回 address(0)。即使我们转移 ETH 而不是 WBTC,债务也不会被清除。这将导致 PoolManager::unlock 中的执行因未清偿债务而 回滚,使兑换未完成。

5. TAKE_ALL

5.1 V4Router

执行再次开始于 V4Router::_handleAction

} else if (action == Actions.TAKE_ALL) {
    (Currency currency, uint256 minAmount) = params.decodeCurrencyAndUint256();
    uint256 amount = _getFullCredit(currency);
    if (amount < minAmount) revert V4TooLittleReceived(minAmount, amount);
    _take(currency, msgSender(), amount);
    return;
}

回顾一下,我们传递的参数是:

params[2] = abi.encode(WBTC_USDC_KEY.currency1, minAmountOut);

在这里,currency1USDC

接下来,函数会调用 DeltaResolver::_getFullCredit,其功能类似于 SETTLE_ALL 流 程中的 _getFullDebt

function _getFullCredit(Currency currency) internal view returns (uint256 amount) {
    int256 _amount = poolManager.currencyDelta(address(this), currency);
    // 如果该金额为负,则应该结算而不是收取。
    if (_amount < 0) revert DeltaNotPositive(currency);
    amount = uint256(_amount);
}

查看 Forge 测试跟踪,我们看到 extload 调用 PoolManager 返回:

├─ [859] POOL_MANAGER::exttload(0x7a546babd112f483b54774c6cda4e5032ea25f89ff1fdd03827ba7f5c9a6386d) [staticcall]
│   └─ ← [Return] 0x00000000000000000000000000000000000000000000000000000002168d151c

此值( 0x000000...2168d151c)为 8_968_279_324 (~9,000e6),与我们在 USDC 中的预期输出相匹配。

因为我们将 minAmount 设为 0,因此滑点检查被跳过(同样,对于主网兑换,这并不可取)。

接下来执行转到 DeltaResolver::_take

function _take(Currency currency, address recipient, uint256 amount) internal {
    if (amount == 0) return;
    poolManager.take(currency, recipient, amount);
}

随即执行进入 PoolManager

5.2 PoolManager

PoolManager::take

function take(Currency currency, address to, uint256 amount) external onlyWhenUnlocked {
    unchecked {
        // negation must be safe as amount is not negative
        _accountDelta(currency, -(amount.toInt128()), msg.sender);
        currency.transfer(to, amount);
    }
}

该函数:

  1. 从瞬时存储中减去该金额,减少记录的债务。

  2. USDC 数量( ~9,000e6)转移到 to,即我们的测试合约( V4Router::_handleAction 中的 msgSender())。

因我们完全结算余额,next = 0,这将使 NonzeroDeltaCount 减少 1,将其设置为 0

此时,所有债务已被清除。

6. 退出 PoolManager::unlock

随着所有操作的完成,执行返回到 BaseActionsRouter::_executeActionsWithoutUnlock,然后返回到 PoolManager::unlock 中的主要回调。

PoolManager::unlock 中的最后两行执行:

if (NonzeroDeltaCount.read() != 0) CurrencyNotSettled.selector.revertWith();
Lock.lock();

由于 NonzeroDeltaCount = 0,检查通过,PoolManager 再次被锁定。

兑换完成!

总结

恭喜你到达终点!我希望这篇逐步讲解能够澄清 Uniswap V4 中兑换如何在内部执行。

有关完整的测试套件,包括原生 ETH 兑换,请查看此 GitHub repo。如果你跟随了这个过程,理解如何处理原生代币应该没有困难。

  • 原文链接: cyfrin.io/blog/uniswap-v...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cyfrin
cyfrin
Securing the blockchain and its users. Industry-leading smart contract audits, tools, and education.