Uniswap v4 数据指南

本文是关于如何解析 Uniswap v4 链上数据的指南。

如何浏览 Uniswap v4 数据

2025 年 2 月 3 日

Uniswap v4 已经到来!凭借新的 singleton 架构、hooks、flash accounting 以及对原生 ETH 的支持,它引入了新的概念,改变了我们查看和分析链上数据的方式。这篇文章提供了一个快速指南,帮助你浏览 v4 数据、开展分析并立即开始发现新的见解。

概述:

  1. 发现: 在哪里找到 Uniswap v4 数据,以及它与之前版本的区别。

  2. 学习: 分析核心功能(如 hooks 和 singleton pools)以衡量交易量、TVL、费用等的实用方法。

  3. 应用: 调整这些见解以构建仪表板、进行更深入的链上研究,并创建针对 v4 创新的新指标。

如果你对方法有疑问或想帮助塑造 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 链上数据回答的问题。

Singleton 实现和 Swaps

Pool 创建

在 Uniswap v4 中,pools 实际上是在 PoolManager contract 中创建的,并通过 ID 来标识。这与之前的版本不同,在之前的版本中,工厂 contract 使用唯一的链上地址创建 pools。Pool 元数据以前在 PairCreatedPoolCreated 事件中找到,现在位于 PoolManager contract 的 Initialize 事件中。

Pool Creation Events 对比

Pool Creation Events 对比

Swapping

在 v4 中,与 v3 类似,amount0amount1 是带符号的整数。但与 v3 不同的是,v4 中的 符号约定 是从 用户的角度 出发的,而不是 pool 的角度。

  • amountX:用户出售 token X(将其发送到 pool)。

  • amountX:用户购买 token X(从 pool 接收)。

同样,这与 v3 的约定 相反,所以要注意!如果你不想手动处理符号逻辑,可以使用标准化的数据集,如 DEX Trades(可在 AlliumDune 上获得),它们已经提供了。

Swapping Events 对比

Swapping Events 对比

有了这些事件,你就可以开始衡量 Uniswap v4 的每日或每月交易量,并将其与之前的版本和其他协议进行比较。

关于 Hooks 的说明:

如果 pool 的 hook 实现了 beforeSwapReturnDeltaafterSwapReturnDelta,则 Swap 事件发出的默认 amount0amount1 将无法准确反映实际的 in/out amounts。dex.trades 的当前实现 尚未 考虑这些特殊情况。有关更多详细信息,请参阅下面的 Hooks 部分。

流动性修改和 TVL

与 v3 类似,v4 使用 NFTs 来管理 LP positions,但 所有流动性变化(mint、burn 等)现在都显示为单个事件:PoolManager 中的 ModifyLiquidity。要计算 TVL,你需要:

  1. 流动性修改(通过 ModifyLiquidity)。

  2. 该修改时间的 pool 当前价格

与 v2 或 v3 不同,直接的 token amounts 并不总是发出。相反,v4 记录了一个 liquidityDelta,你必须进行 tick math 才能计算出有多少 tokens 被锁定。这是一般的公式:

  1. 从最近的 SwapInitialize 事件中找到最近的价格( sqrtPriceX96)。

  2. 将该价格 转换为 tick space。

  3. 根据 pool 的当前价格是 低于高于 还是 LP 的 tick range ,计算 token0/token1 amounts。

  4. 通过考虑 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

Hooks 是 v4 的一个决定性特征。它们使开发人员能够通过实现多达 14 个 hook function任何子集自定义 pools、swap fees 和 LP positions 的交互方式。每个 pool 只能链接到 单个 hook,但单个 hook contract 可以服务于 多个 trading pools。

识别 Hooked 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 个 Hook Flag = 14 个潜在函数

开发人员可以混合和匹配这 14 个 hooks 的任何子集。在内部,hook contract 地址的最后 2 个字节对 14 位 进行编码——每个 hook function 一位——加上左侧的 2 个未使用的位。设置为 1 的位表示启用了该 function (例如,beforeSwapafterAddLiquidity)。

让我们以 CSMM (一个恒定总和做市商自定义曲线 hook) contract 为例。如果我们查看 hook 地址的最后 2 个字节 4888,并将其转换为 16 位,我们将得到 01_00100010001000。请注意,我们应该忽略最左边的 2 位。从第 3 位开始,我们看到第 3 位、第 7 位和第 11 位的值为 1,而其余位为 0。这告诉我们,相应的 hook functions — beforeAddLiquiditybeforeSwapbeforeSwapReturnDelta — 已实现。

下图显示了 14 位中的每一位如何对应于一个 hook function。

Sample CSMM Hook's 14 Hook Flag Bits

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 如何改变价格、费用和流动性

很自然地会问:hooks 实际上如何影响数据中的 swap prices、liquidity 和 fees?简短的回答是,这很大程度上取决于每个 hook 的逻辑。下面是一些示例供你参考。另外,我们还将在不久的将来发布另一个关于浏览 hooks 数据的专用指南,其中包含方法和示例。

MoonFee

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

示例 OverrideFee hook 类似于 MoonFee hook,它可以动态更改 swap fee。但是,与 MoonFee 使用基于月相的动态计算不同,OverrideFee 是伪随机采样的,并且在 swap 发起时通过 hookData 直接提供。

在 OverrideFee contract 中,仅启用 beforeSwap function,这表示 PoolManager 将仅在 swap 发生之前调用此 contract。在执行 beforeSwap 期间,代码从通过 PoolManagerswap() 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 (自定义曲线)

恒定总和做市商 (CSMM) 强制执行 1:1 的 token 交换率,而不是默认的 x*y=k 曲线。它实现了 beforeAddLiquiditybeforeSwapbeforeSwapReturnDelta hooks 来自定义 liquidity handling 和 swap execution。

CSMM 的工作原理

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。

下面以三元表示法编写的公式,确定 amount0amount1。最终结果乘以 -1 以与 v4 的 Swap 事件约定对齐。这是必要的,因为该逻辑最初是从 pool 的角度描述的,而 amount0amount1 应该从用户的角度计算。

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 事件中的 amount0amount1 字段可能为零,因为 hook 覆盖了 swap 逻辑。相反,实际的 swap amounts 必须从 returnDelta 和 swap 参数中导出。要解码涉及 CSMM 的 swaps,你需要:

  1. beforeSwap 调用中提取 returnDelta

  2. 将 int256 value 拆分为两个带符号的 128 位整数:

    • 顶部 128 位:specifiedDelta

    • 底部 128 位:unspecifiedDelta

  3. 使用 zeroForOneexactInput 确定 amount0amount1

请注意,我们可以从调用 beforeSwap 时传入的 SwapParams 字段中提取 zeroForOneamountSpecified 字段。

这是一个 示例查询,演示了 我们如何解码 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

关于返回 Delta

具有 14 个可自定义标志的 hooks 的引入为 AMM 领域的创新开辟了前所未有的可能性,但也为链上数据引入了非标准情况。正如在 CSMM 示例中看到的,实现自定义 swap functions 的 hooks 会覆盖在 Swap 事件中发出的 amount0amount1 values。相反,实际的 swap amounts 必须从 beforeSwap() function 的 returnDelta 中导出。这意味着,对于 Uniswap v4 中的准确交易量计算,仅 Swap 事件不再足够——当启用 BeforeSwapReturnDeltaAfterSwapReturnDelta flags 时,监控 returnDelta values 至关重要。

同样,诸如 AfterAddLiquidityReturnDeltaAfterRemoveLiquidityReturnDelta 之类的 flags 需要跟踪来自相应 afterAddLiquidity()afterRemoveLiquidity() hooks 的 returnDelta,才能正确观察 liquidity changes。此外,诸如 beforeAddLiquidity 之类的 hooks 可以阻止标准 liquidity additions,从而使来自 PoolManagerModifyLiquidity 事件对于 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 的更深入的见解。

v4 的数据集和数据提供商列表

恭喜你走到这一步!你现在拥有开始浏览 Uniswap v4 数据所需的一切。以下是数据平台及其数据集的列表,你可以开始查询以进行研究并回答 v4 的问题。请注意,这是面向数据分析师和研究人员的。如果你是数据工程师或开发人员,请查看 Subgraphs、QuickNode、Alchemy、Ponder、Goldsky 等工具和平台。

  • Allium (网站, 文档)

    • crosschain.dex.trades,用于包含 Uniswap v4 的 DEX trades** 数据

    • crosschain.dex.uniswap_v4_events 用于所有 Uniswap v4 事件数据

  • Dune (网站, 文档)

\\ 如果 pool 的 hook 实现了 beforeSwapReturnDeltaafterSwapReturnDelta,则 Swap 事件发出的默认 amount0amount1 将无法准确反映实际的 in/out amounts。dex.trades 的当前实现 没有 考虑这些特殊情况。有关更多详细信息,请参阅上面的 Hooks 部分。

附:如果你想要 Uniswap v4 概念的视频演示,请查看 Grace DancoXin Wan精彩 DuneCon 2024 演讲

就是这样——你已准备好投入其中。祝你 v4 数据探索愉快!

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

0 条评论

请先 登录 后评论
Uniswap Labs
Uniswap Labs
江湖只有他的大名,没有他的介绍。