Uniswap V3 的 swap 以及 add/remove liquidity 等操作都是即时转账,用户资金会实时流转到目标账户。而在 V4 版本中,所有操作都先在“账本”上进行记账,只有当用户或流动性提供者(LP)主动结算(settle)时,资产才会实际转账。这一设计相比 V3 支持了批量结算、合并多币种操作,大幅提升了效率并降低了 Gas 成本。
先来张结构图:
+------------------------------------------------------------+
| CurrencyDelta |
| (账本:每个账户每种币种的余额变化) |
+-------------------+-----------------+---------------------+
| 用户地址 |币种(Currency)| 余额(delta) |
+-------------------+-----------------+---------------------+
| 0xAAA...AAA | 0x111...111 | +100 |
| 0xAAA...AAA | 0x222...222 | -50 |
| 0xBBB...BBB | 0x111...111 | +200 |
| 0xCCC...CCC | 0x222...222 | +300 |
| ... | ... | ... |
+---------------------+-------------------+-----------------+
账本式结算没有专门的 Solidity 显式存储容器(如 mapping),而是直接用 transient storage(tstore/tload)+ slot 计算来实现“映射”效果。采用transient storage(tstore/tload),高效且 gas 低。
Slot 计算
首先看Slot 计算(_computeSlot)
function _computeSlot(address target, Currency currency) internal pure returns (bytes32 hashSlot) {
assembly ("memory-safe") {
mstore(0, and(target, 0xffffffffffffffffffffffffffffffffffffffff))
mstore(32, and(currency, 0xffffffffffffffffffffffffffffffffffffffff))
hashSlot := keccak256(0, 64)
}
}
为每个 (用户地址, 币种) 生成唯一的 slot,作为账本的 key。
and(target, 0xffffffffffffffffffffffffffffffffffffffff):
Solidity 的address
类型和大多数 token 地址(Currency)都只用 20 字节存储,剩下的高位都是 0。这一步就是把 address 截断为 20 字节,保证不会有高位脏数据,哈希计算更安全、唯一。mstore(0, ...)
:把用户地址的低 20 字节(160 位)写入内存的前 32 字节。mstore(32, ...)
:把币种的低 20 字节写入内存的下一个 32 字节。keccak256(0, 64)
:对这 64 字节(用户地址 + 币种)做哈希,得到唯一的 slot。
这样拼接两个 20 字节的地址,填满 64 字节(32+32),再做 keccak256,确保 slot 唯一且不会冲突。
余额变动的记账流程
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();
}
}
这是记账的入口方法,将某币种的资产变化(delta)记到账本上,归属于 target 地址。维护非零余额地址计数,便于后续清算和管理。
currency.applyDelta用于读取当前余额,累加 delta,写回账本。
function applyDelta(Currency currency, address target, int128 delta)
internal
returns (int256 previous, int256 next)
{
bytes32 hashSlot = _computeSlot(target, currency);
assembly ("memory-safe") {
previous := tload(hashSlot)
}
next = previous + delta;
assembly ("memory-safe") {
tstore(hashSlot, next)
}
}
参数
currency
:币种(如 token0、token1)。target
:用户地址。delta
:本次要累加的余额变化(可以为正或负)。
步骤
- 通过
_computeSlot(target, currency)
计算出唯一 slot,定位账本中该用户该币种的余额位置。 - 用
tload(hashSlot)
读取当前余额(previous
)。 - 计算新余额:
next = previous + delta
。 - 用
tstore(hashSlot, next)
把新余额写回账本。
返回值
previous
:变更前的余额。next
:变更后的余额。
总结
所有资产变化都先记账,只有结算时才真正转账,便于扩展钩子(hook)、奖励、返现等机制。用户/LP 通过 settle()
或相关方法主动结算,将账本余额同步到链上资产,实现真正的 token 转账。账本式设计为后续多资产、多池子批量操作提供了基础。