流动性池是 Uniswap 的核心,也是每次升级的重头戏。如果说从 V2 到 V3,最主要的变化是集中流动性,那么从 V3 到 V4,最主要的变化是流动性池。在 V4 中,流动性池的变更主要有以下几个方面:
下面将逐一详细介绍每个方面的改进,深入探讨它们的实现原理以及影响。
在 Uniswap V3 中,流动性池是通过 UniswapV3Factory 合约部署的,每个池都是一个单独的智能合约。同时,每条链还有一个 Proxy 合约 ( 这个合约在大部分链的地址是0x364484dfb8f2185b90e29fbd10ac96fca8a7e4a7
)。在添加流动性的时候,用户需要对 Proxy 合约发起交易,Proxy 合约再去调用 Pool 合约。在这个过程中,资金要先转入 Proxy 合约,再转入 Pool 变为流动性,最后 Proxy 合约会给用户发放一个 NFT 作为流动性权益的证明。可见这个过程非常繁琐,会消耗很多 gas。另外,用户在交易代币时,如果使用了多跳交易,也要在不同的 Pool 合约中转换,同样要额外花费一些 Gas。
针对这个问题,Uniswap V4 引入单例模式,让所有的 Pool 通过 PoolManager 合约管理。在 PoolManager 有一个变量 _pools
,这个变量保存了所有池的状态。如果要新建一个 Pool,只需要为 _pools
变量增加一个键值对就可以了,不需要部署合约。进行交易的时候,资金也用不着在各个合约之间划转,只要在 PoolManager 中做好记账就可以了。
contract PoolManager is IPoolManager,ProtocolFees,NoDelegateCall,ERC6909Claims,Extsload,Exttload {
// ...
mapping(PoolId id => Pool.State) internal _pools;
// ...
}
这样做的好处很多,我们可以做一个粗糙的类比。比如我们需要进行 BTC 到 USDC 的交易,然后路由计算发现,先把 BTC 兑换成 ETH,再把 ETH 兑换成 USDC 是最省钱的方式。因此我们的兑换路径是 BTC->ETH->USDC。
对于 Uniswap V3 来说,它就像一个菜市场,每一个摊位都要进行一次结算,通知各个 ERC20 进行支付,需要分别结算 BTC,ETH,USDC。
而 Uniswap V4 则像一个大超市,只需要在进门的时候充值,离开的时候提款就可以了,在每个柜台的交易都是挂帐,不会真的结算。只在最后结算 BTC 和 USDC 两个资产。
在这个过程中节省的费用包含两部分:
鉴于 PoolManager 合约的重要性,Uniswap 还搞了个轰轰烈烈的地址挖矿活动[1], 为 PoolManager 选个吉祥地址,也能节省用户的 gas。可见官方对这个合约的重视程度。
单利模式的实现,依赖于瞬态存储技术。瞬态存储是以太坊中新的数据存储方式,在 EIP-1153 中提出,并在 2024 年通过坎昆升级上线。它允许在单个交易执行期间临时存储数据,并在交易结束后自动清除。瞬态存储填补了现有存储机制的空缺,尤其适合那些数据只需在单个交易内有效且需要频繁读写的场景。下面是瞬态存储(Transient Storage)、存储(Storage)、内存(Memory)和调用数据(Calldata)的比较(引用自瞬态存储:Solidity 中的高效临时数据解决方案[2])
特性 | 瞬态存储 | 存储 | 内存 | 调用数据 |
---|---|---|---|---|
存储位置 | 临时存储 | 永久存储 | 临时存储 | 只读存储 |
Gas 成本 | 较低(约 100 gas/ 操作) | 高昂(首次写入至少 20,000 gas) | 较低 | 较低 |
作用域 | 跨函数调用可用 | 跨交易可用 | 仅限于单个函数调用 | 仅限于函数参数 |
状态保持 | 可以保持状态 | 永久保持状态 | 不可保持状态 | 不可保持状态 |
清理成本 | 无需清理 | 需要额外支付 gas | 无需清理 | 无需清理 |
大小限制 | 无明显限制 | 受链上状态限制 | 有限的内存空间 | 有限的输入参数大小 |
适用场景 | 临时数据和计算 | 永久性数据存储 | 临时数据处理 | 函数参数传递 |
正是通过引入瞬态存储(Transient Storage)机制,Uniswap V4 能够在单个交易中以极低的成本存储交易状态,而不需要将中间变量永久性地写入区块链存储。这种设计不仅降低了每次交易的 gas 消耗,也提升了 V4 的交易效率。
除了单例模式外,另一个巨大的变化是手续费。与 V3 相比,V4 的流动性池可以自由设置手续费率,不再局限于几个特定的值。要直观的了解这一点,可以看一下流动性池的创建参数。在 V3 中,创建池的参数是这样的:
address token0: 池的第一个 token address token1: 池的第二个 token,通常第一个和第二个根据地址排序 uint24 fee: 池的手续费率,只能在 1%,0.3%,0.05%,0.01% 中选择
而在 V4 中,创建参数被封装成 PoolKey
对象。这个对象的字段包括:
Currency currency0: token 0。(Currency 类型是 address 类型的别名 ) Currency currency1: token 1。 uint24 fee: 池的手续费率,可以设置为不超过 100% 的值,如果设置为 0x800000
,表示池使用动态手续费。int24 tickSpacing: 流动性的 tickspacing。可以自由指定,从 2 到 32766 都可以。 IHooks hooks: Pool 的 hook,想要详细了解可以查看之前的文章[3]。
从这些参数可见,流动性池的初始化参数主要包含两部分: 代币和手续费。
V4 的手续费设置有两个参数,fee
和 tickSpacing
,而 V3 只有fee
。对于手续费,V3 只能设置四种值 (1%,0.3%,0.05%,0.01%),相比于 V2 只能设置 0.3%,已经有了一些进步。但是在 V4 中,手续费率可以在小于 100% 的数字中任意挑选,这让池的创建由了更高的自由度,可以根据代币的特点设置更精准的手续费值。
tickSpacing
是流动性的粒度。较小的值可提高价格精度;然而,较小的值将导致 swap 交易更频繁地跨越 tick,从而产生更高的 gas 成本。在 V3 中,tick spacing 是由手续费率决定的,对应关系如下:
Fee | Fee Value | Tick Spacing |
---|---|---|
0.01% | 100 | 1 |
0.05% | 500 | 10 |
0.30% | 3000 | 60 |
1.00% | 10_000 | 200 |
而 V4 取消了这种绑定关系,因此 fee 和 tick spacing 变成了两个参数分开设置,在 V4 中,tick spacing 可以取从 2~32766 的任意整数。至于这个值取大还是取小,就要权衡价格精度以及交易频率了。比如对于稳定币来说,tick spacing 要尽量小,而对于 BTC 这种价格接近十万的代币来说,tick spacing 可以大一些以节约 gas。将费率和 tick spacing 解绑,相当于解除了代币价格和精度的绑定关系,这会让高价格的币种受益。
这种任意设置带来了一个问题。对于相同的交易对,每个手续费,每种 tick spacing 都是一个新的池,因此池的数量会大大增加。比如在 V3 中,USDC-ETH 就存在 0.05% 和 0.3% 以及 1% 三个池,而且三个池都很活跃,流动性排名都在前 50。到了 V4,由于费率有 1000,000 种可能,V4 中 USDC-ETH 池很容易出现上百个。要是考虑到不同的 Hook 也算新的池,未来的池就更多了。这必然会分散每个池的流动性。
动态手续费是 V4 中除 Hook 以外最大的变化了,意义不亚于 V3 的集中流动性。所谓「动态手续费」,是指流动性池的手续费不再固定为某个具体值,而是可以根据市场的具体情况随时进行调整。这种灵活性使得动态手续费能够衍生出多种使用场景,为 Uniswap 带来更多的创新应用和策略,从而提高整个协议的效率和适应性。以下是一些可能的动态手续费调整场景:
通过上述例子可以看出,动态手续费的引入不仅为流动性池提供了更多的灵活性,也为流动性提供者和用户带来了不同的收益与节省机会。对于流动性提供者来说,动态手续费能够在市场波动较大的时候提供更高的收益,从而激励他们在高风险时期提供更多的流动性;而对于普通用户来说,手续费的动态调整能够根据交易时段、市场情况或网络拥堵程度进行优化,减少交易成本,从而提高交易体验和平台的整体吸引力。
总之,动态手续费为 Uniswap 引入了一种更为灵活和智能的费用管理方式,不仅增强了市场适应性,还为流动性提供者和用户提供了更多的选择和机会,推动去中心化交易协议在日益复杂的市场环境中不断向前发展。
下面的话引用了 Uniswap 官方的说法,描述了他们眼中动态手续费的场景:
改进波动性的定价:根据市场波动性调整费用,类似于传统交易所调整买卖差价。 根据订单定价:更加准确地定价不同类型的交易(例如,套利交易与非信息性交易)。 提升市场效率与稳定性:费用可以根据实时市场状况进行调整,优化流动性提供者和交易者的利益。动态费用有助于通过实时调整激励措施来抑制极端市场波动。 提升资本效率与流动性提供者回报:通过优化费用,池子能够吸引更多流动性并促进更高效的交易。更精准的费用定价可能带来更好的回报,进而吸引更多资本进入池子。 更好的风险管理:在高波动时期,费用可以提高,以保护流动性提供者免受暂时性损失。 可定制的策略:为特定代币对或市场细分提供复杂的费用策略。
动态手续费的实现依赖于流动性池的 Hook。Hook 是 V4 的新概念,它是一个单独的合约,可以为流动性池增加额外的逻辑。通过 Hook,流动性池可以在交易前后插入一些自定义函数,在这些函数中都可以更改手续费率。因此,更改手续费的方式是非常灵活的,比如在添加移除流动性的时候,或者 swap 的前后都可以计算新的手续费,并实时更改池的手续费率
如果要允许流动性池使用动态手续费,需要在创建池的时候,将费率设置为0x80000
, 也就是LPFeeLibrary.DYNAMIC_FEE_FLAG
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
PoolKey memory pool = PoolKey({
currency0: currency0,
currency1: currency1,
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
tickSpacing: tickSpacing,
hooks: hookContract
});
IPoolManager(manager).initialize(pool, startingPrice);
而改变动态手续费率有两种办法。一是在 Hook 中调用IPoolManager.updateDynamicLPFee(key,newValue)
,永久改变池的手续费率。这个函数可以在 Hook 的任意挂入点调用。比如下面这个例子:
contract HookExample {
IPoolManager public immutable manager;
constructor(IPoolManager _manager) {
manager = _manager;
}
/// 在初始化池的时候设置初始的手续费
function afterInitialize(
address sender,
PoolKey calldata key,
uint160 sqrtPriceX96,
int24 tick,
bytes calldata hookData
) external returns (bytes4) {
uint24 INITIAL_FEE = 3000; // 0.30%
manager.updateDynamicLPFee(key,INITIAL_FEE);
return this.afterInitialize.selector;
}
/// 在添加流动性之前更新手续费
function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
// 计算得到新手续费
uint24 newFee = XXX;
manager.updateDynamicLPFee(key,newFee);
}
}
注意: 在创建池后,默认费率是 0,所以要在 Hook 的
afterInitialize
为手续费设置一个初始值
另一种方式是临时更改费率。在 Hook 的beforeSwap
中,可以返回一个费率,然后此次 swap 交易就会使用这个新的费率。而池的手续费率不会更改,其他的交易也不会受影响。
import {BeforeSwapDelta,BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
contract HookExample {
// ...
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) external returns (bytes4,BeforeSwapDelta,uint24) {
// 通过设置 uint24 的第二高位,设置临时手续费
uint24 fee = 3000 | LPFeeLibrary.OVERRIDE_FEE_FLAG;
return (this.beforeSwap.selector,BeforeSwapDeltaLibrary.ZERO_DELTA,fee);
}
}
不过对于动态手续费,有两个概念容易混淆。在 Hook 中也可以额外手续一部分手续费,它和池的手续费不冲突,两个可以一起收。事实上,流动性池的手续费一般会叫做 LP fee,而 Hook 收的手续费会叫 Hook fee。一般提到动态手续费是指流动性池的手续费。
作为领先的 AMM 协议,Uniswap V4 的流动性池更新再次彰显了其在去中心化交易协议领域的创新与领先地位。这一系列的变革不仅增强了协议的灵活性和可扩展性,还显著提升了其市场竞争力,使得 Uniswap V4 在许多方面将对手甩开,继续保持在 DeFi 生态中的主导地位。
轰轰烈烈的地址挖矿活动: https://v4-address.uniswap.org/
[2]瞬态存储:Solidity 中的高效临时数据解决方案: https://learnblockchain.cn/article/9847
[3]之前的文章: https://github.com/antalpha-labs/zelos
笔者:Steven Sun,Zelos
点击左下方「阅读原文」/「Read More」,获取更多相关信息
Antalpha Labs 是一个非盈利的 Web3 开发者社区,致力于通过发起和支持开源软件推动 Web3 技术的创新和应用。
官网:https://labs.antalpha.com
Twitter:https://twitter.com/Antalpha_Labs
Youtube:https://www.youtube.com/channel/UCNFowsoGM9OI2NcEP2EFgrw
联系我们:hello.labs@antalpha.com
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。