本文是关于如何解析 Uniswap v4 链上数据的指南。
如何浏览 Uniswap v4 数据
2025 年 2 月 3 日
Uniswap v4 已经到来!凭借新的 singleton 架构、hooks、flash accounting 以及对原生 ETH 的支持,它引入了新的概念,改变了我们查看和分析链上数据的方式。这篇文章提供了一个快速指南,帮助你浏览 v4 数据、开展分析并立即开始发现新的见解。
概述:
发现: 在哪里找到 Uniswap v4 数据,以及它与之前版本的区别。
学习: 分析核心功能(如 hooks 和 singleton pools)以衡量交易量、TVL、费用等的实用方法。
应用: 调整这些见解以构建仪表板、进行更深入的链上研究,并创建针对 v4 创新的新指标。
如果你对方法有疑问或想帮助塑造 v4 数据的标准,请联系!
自 v1 的恒定乘积 AMM 设计以来,Uniswap 已经走过了漫长的道路,在 v1 中你只能交换 ETH 和单个 ERC20。v2 带来了 token-to-token 交换和 flash swaps,扩展了用例和流动性机会。然后 v3 引入了集中流动性和多个费用等级,提高了 LP 的资本效率并改善了交易者的执行。
现在,v4 保留了 v3 的优势,但增加了新的功能,例如 singleton 架构 和 hooks,从而实现了更高效和更具创意的 AMM 表达。这些创新也 复杂化 了数据格局:
SingletonPoolManager取代了工厂创建的 pool contracts,从而改变了我们跟踪 pool creation 和 liquidity events 的方式。
Hooks 可以覆盖swap amounts、fees 和 liquidity behavior——如果未考虑 hook 逻辑,则默认的链上事件可能会不完整。
本指南旨在阐明这些新的复杂性,向你展示在哪里可以找到 v4 数据、如何解释它以及需要注意什么。下面,我们重点介绍这些变化如何影响数据收集、分析,以及最终,你可以使用 v4 链上数据回答的问题。
在 Uniswap v4 中,pools 实际上是在 PoolManager
contract 中创建的,并通过 ID 来标识。这与之前的版本不同,在之前的版本中,工厂 contract 使用唯一的链上地址创建 pools。Pool 元数据以前在 PairCreated
或 PoolCreated
事件中找到,现在位于 PoolManager
contract 的 Initialize
事件中。
Pool Creation Events 对比
在 v4 中,与 v3 类似,amount0
和 amount1
是带符号的整数。但与 v3 不同的是,v4 中的 符号约定 是从 用户的角度 出发的,而不是 pool 的角度。
负 amountX
:用户出售 token X(将其发送到 pool)。
正 amountX
:用户购买 token X(从 pool 接收)。
同样,这与 v3 的约定 相反,所以要注意!如果你不想手动处理符号逻辑,可以使用标准化的数据集,如 DEX Trades(可在 Allium 或 Dune 上获得),它们已经提供了。
Swapping Events 对比
有了这些事件,你就可以开始衡量 Uniswap v4 的每日或每月交易量,并将其与之前的版本和其他协议进行比较。
关于 Hooks 的说明:
如果 pool 的 hook 实现了
beforeSwapReturnDelta
或afterSwapReturnDelta
,则Swap
事件发出的默认amount0
和amount1
将无法准确反映实际的 in/out amounts。dex.trades
的当前实现 尚未 考虑这些特殊情况。有关更多详细信息,请参阅下面的 Hooks 部分。
与 v3 类似,v4 使用 NFTs 来管理 LP positions,但 所有流动性变化(mint、burn 等)现在都显示为单个事件:PoolManager
中的 ModifyLiquidity
。要计算 TVL,你需要:
流动性修改(通过 ModifyLiquidity
)。
该修改时间的 pool 当前价格。
与 v2 或 v3 不同,直接的 token amounts 并不总是发出。相反,v4 记录了一个 liquidityDelta
,你必须进行 tick math 才能计算出有多少 tokens 被锁定。这是一般的公式:
从最近的 Swap
或 Initialize
事件中找到最近的价格( sqrtPriceX96
)。
将该价格 转换为 tick space。
根据 pool 的当前价格是 低于、高于 还是 在 LP 的 tick range 内,计算 token0/token1 amounts。
通过考虑 token decimals 和 price oracles,转换为美元(或其他参考货币)。
下面是逻辑的高级代码段(以 Ethereum Sepolia 数据为例)。完整的查询可以在这里找到。非常感谢 Grace Danco 将其组合在一起!
注意:如果你发现 Q notation 或 sqrtPriceX96
不熟悉。阅读“Uniswap v3 Math 入门”第 1 部分 和第 2 部分,或与 ChatGPT 聊天。
-- 1) Get the most recent sqrtPriceX96 (p) before the ModifyLiquidity event
WITH get_recent_sqrtPriceX96 AS (
SELECT *
FROM (
SELECT
ml.*,
i.currency0 AS token0,
i.currency1 AS token1,
COALESCE(s.evt_block_time, i.evt_block_time) AS most_recent_time,
COALESCE(s.sqrtPriceX96, i.sqrtPriceX96) AS sqrtPriceX96,
ROW_NUMBER() OVER (
PARTITION BY ml.id, ml.evt_block_time
ORDER BY CASE WHEN s.sqrtPriceX96 IS NOT NULL
THEN s.evt_block_time
ELSE i.evt_block_time END DESC
) AS rn
FROM uniswap_v4_sepolia.PoolManager_evt_ModifyLiquidity ml
LEFT JOIN uniswap_v4_sepolia.PoolManager_evt_Swap s
ON ml.evt_block_time > s.evt_block_time AND ml.id = s.id
LEFT JOIN uniswap_v4_sepolia.PoolManager_evt_Initialize i
ON ml.evt_block_time >= i.evt_block_time AND ml.id = i.id
) tbl
WHERE rn = 1
),
-- 2) Convert sqrtPrice to ticks, handle range math
prep_for_calculations AS (
SELECT
...
sqrtPriceX96,
LOG(sqrtPriceX96 / POWER(2, 96), 10) / LOG(1.0001, 10) AS tickCurrent,
SQRT(POWER(1.0001, tickLower)) AS sqrtRatioL,
SQRT(POWER(1.0001, tickUpper)) AS sqrtRatioU,
...
FROM get_recent_sqrtPriceX96
),
-- 3) Compute how many token0/token1 are locked based on liquidityDelta
base_amounts AS (
SELECT
...
CASE WHEN sqrtPrice <= sqrtRatioL THEN liquidityDelta * ((sqrtRatioU - sqrtRatioL)/(sqrtRatioL*sqrtRatioU))
WHEN sqrtPrice >= sqrtRatioU THEN 0
ELSE liquidityDelta * ((sqrtRatioU - sqrtPrice)/(sqrtPrice*sqrtRatioU))
END as amount0,
CASE WHEN sqrtPrice <= sqrtRatioL THEN 0
WHEN sqrtPrice >= sqrtRatioU THEN liquidityDelta*(sqrtRatioU - sqrtRatioL)
ELSE liquidityDelta*(sqrtPrice - sqrtRatioL)
END as amount1
...
FROM prep_for_calculations
)
-- 4) Convert to decimal/fiat
SELECT
...
amount0 / POWER(10, tk0.decimals) * usd_price AS amount0_div,
amount1 / POWER(10, tk1.decimals) * used_price AS amount1_div
...
在 v2 和 v3 中,pools 具有预定义的费用等级(例如,0.3%、0.05%)。在 v4 中,fees 可以是动态的,并且可以通过 hooks 进行调整。这种动态费用使用一个固定的常量来表示,即 24 位标志 1000 0000 0000 0000 0000 0000
,在整数中表示为 8,388,608。因此,如果你在 Initialize
事件中看到 fee = 8388608
,则表示创建了一个 动态费用 pool。虽然 Swap
事件的 fee
字段始终显示总 swap fee (即 LP fee 和 protocol fee 的总和),以百分之一为一个基点,但如果 protocol fee 开关打开,则区分 LP 和 protocol fees 需要额外的工具。
Hooks 是 v4 的一个决定性特征。它们使开发人员能够通过实现多达 14 个 hook function 的 任何子集 来 自定义 pools、swap fees 和 LP positions 的交互方式。每个 pool 只能链接到 单个 hook,但单个 hook contract 可以服务于 多个 trading pools。
当通过 PoolManager
中的 Initialize
事件创建 pool 时,hooks 字段存储:
一个 空地址,表示 没有 hook 附加,或者
一个 非空地址,表示 hook contract。
下面是一个快速的示例查询,显示了 Sepolia 上有多少 hooked pools,以及其中有多少具有动态费用:
SELECT
COUNT(*) AS total_pools,
SUM(CASE WHEN hooks <> 0x0000000000000000000000000000000000000000 THEN 1 ELSE 0 END) AS hooked_pools,
SUM(CASE WHEN fee = 8388608 THEN 1 ELSE 0 END) AS dynamic_fee_pools
FROM uniswap_v4_sepolia.PoolManager_evt_Initialize
开发人员可以混合和匹配这 14 个 hooks 的任何子集。在内部,hook contract 地址的最后 2 个字节对 14 位 进行编码——每个 hook function 一位——加上左侧的 2 个未使用的位。设置为 1 的位表示启用了该 function (例如,beforeSwap
、afterAddLiquidity
)。
让我们以 CSMM (一个恒定总和做市商自定义曲线 hook) contract 为例。如果我们查看 hook 地址的最后 2 个字节 4888
,并将其转换为 16 位,我们将得到 01_00100010001000
。请注意,我们应该忽略最左边的 2 位。从第 3 位开始,我们看到第 3 位、第 7 位和第 11 位的值为 1,而其余位为 0。这告诉我们,相应的 hook functions — beforeAddLiquidity
、beforeSwap
和 beforeSwapReturnDelta
— 已实现。
下图显示了 14 位中的每一位如何对应于一个 hook function。
Sample CSMM Hook's 14 Hook Flag Bits
示例查询通过提取最后 2 个字节、转换为二进制并应用按位与来解码 hook 地址以查找已启用的 hook flags:
WITH hooked_pools AS (
SELECT
-- ...
hooks
FROM uniswap_v4_sepolia.PoolManager_evt_Initialize i
WHERE i.hooks IN (
0x1EC90889C6633A0d01932fbf83be93c22d194888, -- CSMM
0x9067ABa6C8D31113910B41e386c4Ea52bAFBC080, -- MoonFee
0xE9FeDDd1C31C6F4AE05F58a1094daC58A5Aa4080 -- OverrideFee
)
),
conversion AS (
SELECT
CASE
WHEN hooks = 0xe9feddd1c31c6f4ae05f58a1094dac58a5aa4080 THEN 'OverrideFee'
WHEN hooks = 0x1ec90889c6633a0d01932fbf83be93c22d194888 THEN 'CSMM'
WHEN hooks = 0x9067aba6c8d31113910b41e386c4ea52bafbc080 THEN 'MoonFee'
END AS hook_name,
to_base(CAST(varbinary_to_uint256(substr(hooks, -2)) AS BIGINT), 2) AS bits16
FROM hooked_pools
)
SELECT DISTINCT
hook_name,
bits16,
bitwise_and(from_base(bits16, 2), from_base('0000000010000000', 2)) != 0 AS _before_swap,
bitwise_and(from_base(bits16, 2), from_base('0000000000001000', 2)) != 0 AS _before_swap_returns_delta,
bitwise_and(from_base(bits16, 2), from_base('0000100000000000', 2)) != 0 AS _before_add_liquidity
FROM conversion
一旦你确定了 pool 的 hook,你就可以探索以下问题:
哪些 hooked pools 推动了最多的交易量或流动性增长?
不同的 hook 配置如何影响 LP 盈利能力或用户 swap behavior?
很自然地会问:hooks 实际上如何影响数据中的 swap prices、liquidity 和 fees?简短的回答是,这很大程度上取决于每个 hook 的逻辑。下面是一些示例供你参考。另外,我们还将在不久的将来发布另一个关于浏览 hooks 数据的专用指南,其中包含方法和示例。
MoonFee 是一个 动态费用 hook。它不是在 pool 初始化期间设置的静态费用等级,而是根据月相动态调整 swap fees。此 hook 仅实现 beforeSwap
function,并在月亮从新月到满月期间增加 swap fee,然后在月亮从满月到新月期间线性降低 swap fee。MoonFee 通过调用 PoolManager’
s updateDynamicLPFee
动态修改 pool 的 swap fee。可以使用 查询 来查看 MoonFee hook 的 pools 发出的 Swap
事件数据,以查看由于动态调整 swap fee,发出的 fee
如何根据月相动态变化。
示例 OverrideFee hook 类似于 MoonFee hook,它可以动态更改 swap fee。但是,与 MoonFee 使用基于月相的动态计算不同,OverrideFee 是伪随机采样的,并且在 swap 发起时通过 hookData
直接提供。
在 OverrideFee contract 中,仅启用 beforeSwap
function,这表示 PoolManager
将仅在 swap 发生之前调用此 contract。在执行 beforeSwap
期间,代码从通过 PoolManager
的 swap()
function 发起 swap 的实体提供的 hookData
中提取 fee。然后,返回此用户指定的 fee 以及 override flag,指示 PoolManager 将新 fee 应用于即将到来的 swap。与 MoonFee 不同,OverrideFee 的 fee value 是短暂的,并且未存储在 PoolManager 的 contract 状态中。
这种行为可以在数据中观察到。一个 query 可以展示 如何调用 beforeSwap
function 并传入包含 override fee 的 hookData
。随后,在由 Hooked with OverrideFee 的 pools 发出的 Swap
事件中,swap fee 会被动态覆盖。
恒定总和做市商 (CSMM) 强制执行 1:1 的 token 交换率,而不是默认的 x*y=k
曲线。它实现了 beforeAddLiquidity
、beforeSwap
和 beforeSwapReturnDelta
hooks 来自定义 liquidity handling 和 swap execution。
beforeSwap
此 hook 拦截 swaps 并直接管理 token input/output。它计算以 1:1 交换要 mint 和 burn 的 token amount,并返回 生成的 deltas(returnDelta
) 到 PoolManager
用于 accounting。Swap direction 由 zeroForOne
参数确定,该参数指示用户是将 token0 交换为 token1 (true) 还是将 token1 交换为 token0 (false)。此外,amountSpecified
参数区分 ExactInput 和 ExactOutput swaps:负 value 表示用户指定 input amount 的 ExactInput swap,而正 value 表示用户指定 output amount 的 ExactOutput swap。返回的 returnDelta
用于根据 swap direction 和 input/output 样式结算 token balances。
下面以三元表示法编写的公式,确定 amount0
和 amount1
。最终结果乘以 -1 以与 v4 的 Swap
事件约定对齐。这是必要的,因为该逻辑最初是从 pool 的角度描述的,而 amount0
和 amount1
应该从用户的角度计算。
amount0 = -1 * (zeroForOne and exact-input) ? specifiedDelta : unspecifiedDelta
amount1 = -1 * (oneForZero and exact-input) ? specifiedDelta : unspecifiedDelta
流动性处理
标准 Uniswap v4 liquidity additions 被 beforeAddLiquidity
hook 阻止。取而代之的是,CSMM 需要通过其自定义 addLiquidity
function 存入等量的 tokens,并在 hook 的内部 balances 中跟踪 liquidity,而不是通常的 PoolManager
日志中跟踪 liquidity。这意味着 liquidity (或 TVL) 将位于 hooks contract 中,而不是位于 PoolManager contract 中。
Swap
事件中的 amount0
和 amount1
字段可能为零,因为 hook 覆盖了 swap 逻辑。相反,实际的 swap amounts 必须从 returnDelta
和 swap 参数中导出。要解码涉及 CSMM 的 swaps,你需要:
从 beforeSwap
调用中提取 returnDelta
。
将 int256 value 拆分为两个带符号的 128 位整数:
顶部 128 位:specifiedDelta
。
底部 128 位:unspecifiedDelta
。
使用 zeroForOne
和 exactInput
确定 amount0
和 amount1
。
请注意,我们可以从调用 beforeSwap
时传入的 SwapParams
字段中提取 zeroForOne
、amountSpecified
字段。
这是一个 示例查询,演示了 我们如何解码 CSMM swaps 的 returnDelta
,其中包含详细的内联注释。
-- Get Swap interactions with CSMM (constant product market maker) hook
with selected_pools as (
select id as pool_id -- 28
...
from uniswap_v4_sepolia.PoolManager_evt_Initialize
where hooks = 0x1EC90889C6633A0d01932fbf83be93c22d194888 -- CSMM
)
, fixed_bytes as (
select s.evt_block_time
...
, b.output_1 as returnDelta
, CAST(
json_extract(params, '$.zeroForOne') AS boolean
) AS zeroForOne
, json_extract(params, '$.amountSpecified') as amountSpecified
, CAST(b.output_1 AS varbinary) AS int256_varbinary
from uniswap_v4_sepolia.PoolManager_evt_Swap s
left join uniswap_v4_hooks_sepolia.CSMM_call_beforeSwap b on b.call_success
...
)
, wrangled as (
select *
-- 1) Check sign bit in the "top 128 bits" first byte
, CASE
WHEN bitwise_and(
varbinary_to_bigint(varbinary_substring(int256_varbinary, 1, 1)),
from_base('80', 16) -- 0x80 as decimal 128
) = from_base('80', 16)
THEN varbinary_to_int256(
varbinary_concat(
from_hex('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), -- 16 bytes of 0xFF
varbinary_substring(int256_varbinary, 1, 16) -- the top 16 bytes
)
)
ELSE varbinary_to_int256(
varbinary_concat(
from_hex('0x00000000000000000000000000000000'), -- 16 bytes of 0x00
varbinary_substring(int256_varbinary, 1, 16)
)
)
END AS high_bits
-- 2) Check sign bit in the "bottom 128 bits" first byte
, CASE
WHEN bitwise_and(
varbinary_to_bigint(varbinary_substring(int256_varbinary, 17, 1)),
from_base('80', 16)
) = from_base('80', 16)
THEN varbinary_to_int256(
varbinary_concat(
from_hex('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), -- 16 bytes of 0xFF
varbinary_substring(int256_varbinary, 17, 16) -- the bottom 16 bytes
)
)
ELSE varbinary_to_int256(
varbinary_concat(
from_hex('0x00000000000000000000000000000000'), -- 16 bytes of 0x00
varbinary_substring(int256_varbinary, 17, 16)
)
)
END AS low_bits
from fixed_bytes order by 1 desc
)
, base_data as (
select
evt_tx_hash
, swap_fee_pct
, cast(cast(amountSpecified as varchar) as int256) / 1e18 as amountSpecified_input
, zeroForOne
-- // bool exactInput = params.amountSpecified < 0
, CASE WHEN cast(cast(amountSpecified as varchar) as int256) < 0 THEN TRUE ELSE FALSE END AS exactInput
, high_bits / 1e18 as specifiedCurrency
, low_bits / 1e18 as unspecifiedCurrency
from wrangled
)
/*
Formula for calculating amount0 and amount1 from returned delta
amount0 = (zeroForOne and exact-input) ? specifiedDelta : unspecifiedDelta
amount1 = (oneForZero and exact-input) ? specifiedDelta : unspecifiedDelta
*/
SELECT
evt_tx_hash
, swap_fee_pct
, amountSpecified_input
, zeroForOne
, exactInput
, specifiedCurrency as specifiedDelta
, unspecifiedCurrency as unspecifiedDelta
-- Calculate amount0 and amount1 with formula
-- Since the code is written from the pool's perspective, following v4 convention on amount0 amount1
-- They should be from the user's perspective, hence flipping the sign by muliplying with -1
, -1 * CASE
WHEN zeroForOne AND exactInput THEN specifiedCurrency
ELSE unspecifiedCurrency
END AS amount0
, -1 * CASE
WHEN NOT (zeroForOne AND exactInput) THEN specifiedCurrency
ELSE unspecifiedCurrency
END AS amount1
FROM base_data
具有 14 个可自定义标志的 hooks 的引入为 AMM 领域的创新开辟了前所未有的可能性,但也为链上数据引入了非标准情况。正如在 CSMM 示例中看到的,实现自定义 swap functions 的 hooks 会覆盖在 Swap 事件中发出的 amount0
和 amount1
values。相反,实际的 swap amounts 必须从 beforeSwap()
function 的 returnDelta 中导出。这意味着,对于 Uniswap v4 中的准确交易量计算,仅 Swap
事件不再足够——当启用 BeforeSwapReturnDelta
或 AfterSwapReturnDelta
flags 时,监控 returnDelta
values 至关重要。
同样,诸如 AfterAddLiquidityReturnDelta
和 AfterRemoveLiquidityReturnDelta
之类的 flags 需要跟踪来自相应 afterAddLiquidity()
和 afterRemoveLiquidity()
hooks 的 returnDelta
,才能正确观察 liquidity changes。此外,诸如 beforeAddLiquidity
之类的 hooks 可以阻止标准 liquidity additions,从而使来自 PoolManager
的 ModifyLiquidity
事件对于 TVL 计算而言是不完整的。直接添加到 hook 储备的 liquidity 绕过了 PoolManager
的跟踪,因为它没有锁定在其 contract 中。
虽然 DeFi 仍处于早期阶段,但 hooks 空间甚至更加新兴。随着越来越多的 hooks 部署在 v4 pools 中,我们将看到新模式的出现,例如为不同的 stablecoin 生态系统量身定制的 stable pools、无预言机 lending protocols、自动 liquidity management strategies 以及 tokenized treasuries 或预测市场等新兴资产类别的市场。这种不断扩展的 v4 数据格局突出了日益增长的对标准化数据实践的需求,以准确跟踪 swap prices、liquidity changes 和 fees。对于任何分析链上市场的人来说,这是一个令人兴奋的前沿,它提供了无限的创新机会和对 decentralized finance 的更深入的见解。
恭喜你走到这一步!你现在拥有开始浏览 Uniswap v4 数据所需的一切。以下是数据平台及其数据集的列表,你可以开始查询以进行研究并回答 v4 的问题。请注意,这是面向数据分析师和研究人员的。如果你是数据工程师或开发人员,请查看 Subgraphs、QuickNode、Alchemy、Ponder、Goldsky 等工具和平台。
crosschain.dex.trades
,用于包含 Uniswap v4 的 DEX trades** 数据
crosschain.dex.uniswap_v4_events
用于所有 Uniswap v4 事件数据
dex.trades
用于包含 Uniswap v4 的 DEX trades** 数据
\\ 如果 pool 的 hook 实现了 beforeSwapReturnDelta
或 afterSwapReturnDelta
,则 Swap
事件发出的默认 amount0
和 amount1
将无法准确反映实际的 in/out amounts。dex.trades
的当前实现 没有 考虑这些特殊情况。有关更多详细信息,请参阅上面的 Hooks 部分。
附:如果你想要 Uniswap v4 概念的视频演示,请查看 Grace Danco 和 Xin Wan 的 精彩 DuneCon 2024 演讲。
就是这样——你已准备好投入其中。祝你 v4 数据探索愉快!
- 原文链接: uniswapfoundation.mirror...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!