本文深入探讨了Uniswap V4的兑换机制,特别关注了引入的闪电会计模型及其在交换执行中的应用。文章通过详细的代码示例和逐步分析,展示了交易的执行流程、会计管理及状态控制等关键概念,适合希望深入了解Uniswap V4的开发者和技术爱好者。
深入探讨 Uniswap V4 的兑换机制,详细了解闪电会计(flash accounting)、瞬时存储和执行流程,通过详细的代码示例和分析进行说明。
我一直在探索 Uniswap V4,以更好地理解其兑换执行。为了加深我的理解,我分析了驱动兑换的代码,专注于在 V4 中引入的新“闪电会计”模型:如何使用瞬时存储创建、结算和跟踪债务,以及何时实际移动价值。如果你对 Uniswap V4 兑换的内部工作原理感到好奇,特别是从会计的角度来看,这个指南就是为你准备的。让我们深入探讨吧!
为了演示这一点,我们将使用一个 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
的兑换,可以在 这里 找到。你还可以找到一个 图表,作为你阅读时的便捷参考。
兑换执行从我们测试中的以下几行开始:
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。
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.sender
( UniversalRouter
)。这在 Uniswap V4 中引入了执行流程中的一个关键变化:
通过 unlock 解锁 PoolManager
PoolManager
将执行返还给 msg.sender
msg.sender
与 Uniswap V4 进行交互(例如,执行兑换/添加/移除流动性)
会计被验证为正确
PoolManager
再次被锁定
在我们的例子中,msg.sender
是 BaseActionsRouter
,它是 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)
);
会计如下:
SWAP_EXACT_IN_SINGLE
:创建从调用者到池的输入代币( WBTC
)的债务,以及从池到调用者的输出代币( USDC
)的债务。
SETTLE_ALL
:结算从调用者到池的债务。
TAKE_ALL
:从池收集应得的输出代币并发送给调用者。
现在,让我们探讨每个动作。
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
执行实际的兑换。
兑换本身发生在 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);
}
该函数:
检索池状态。
执行任何预兑换Hook,在我们的案例中由于我们的池没有Hook,因此不适用。
执行兑换。
应用后兑换Hook,同样,在我们的案例中不适用。
记录余额变化。
兑换结果为 BalanceDelta
(压缩数据类型,表示两个 int128
值打包到单个 int256
中),记录了 UniversalRouter
与 PoolManager
之间的债务。
兑换完成后,此余额 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
(因此我们)欠 PoolManager
的 WBTC
。
PoolManager
欠 UniversalRouter
的 USDC
。
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
。
V4Router::_swapExactInputSingle
的最后一步是滑点检查:
if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived(params.amountOutMinimum, amountOut);
我们在测试中将 params.amountOutMinimum
设为 0,因此此检查通过。然而,在实际兑换中永远不应这样做,因为这会使你的兑换易受滑点滥用的影响。
至此,SWAP_EXACT_IN_SINGLE
完全执行!
下一步是清理债务,以便我们可以退出 PoolManager::unlock
并完成兑换。
SETTLE_ALL
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,代表我们欠 PoolManager
的 WBTC
数量。
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
中的瞬时存储,以确保其正确跟踪即将到来的余额。
PoolManager
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
余额。
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));
我们已经将债务代币转账给池。现在是时候结算会计了。
V4Router
回到 DeltaResolver::_settle
,执行现在进入 poolManager.settle()
。
PoolManager
PoolManager::settle
和 PoolManager::_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
中的执行因未清偿债务而 回滚,使兑换未完成。
TAKE_ALL
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);
在这里,currency1
是 USDC
。
接下来,函数会调用 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
。
PoolManager
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);
}
}
该函数:
从瞬时存储中减去该金额,减少记录的债务。
将 USDC
数量( ~9,000e6
)转移到 to
,即我们的测试合约( V4Router::_handleAction
中的 msgSender()
)。
因我们完全结算余额,next = 0
,这将使 NonzeroDeltaCount
减少 1
,将其设置为 0
。
此时,所有债务已被清除。
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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!