使用 shadow 构建强大的 Uniswap V3 数据面板

  • Shadow
  • 更新于 18小时前
  • 阅读 132

使用 shadow 构建强大的 Uniswap V3 数据面板

概述

Shadow 与 Uniswap 合作,为 Uniswap v3 池开发了自定义的 shadow 事件,从而解锁了更丰富的池活动和流动性动态数据集。

我们构建了一个仪表板(univ3.xyz),利用这个数据集和我们最近推出的实时数据库同步 ,展示你可以用 Shadow 构建的内容。

在这篇文章中,我们将详细介绍这些 Uniswap v3 shadow 事件,它们解锁的新数据,以及它们是如何用来生成每个仪表板图表的。

Shadow 解锁的内容

Shadow 赋予你将链外事件日志添加到任何部署的智能合约的超级能力,并提供一个易于使用的托管平台,消除复杂数据管道和其他节点基础设施的需求。

Shadow 为顶级加密工程和数据团队节省了宝贵的时间——链上数据问题曾经需要数周解决,现在缩短为几小时甚至几分钟。

借助 Shadow,一个人可以在一个下午构建一个强大的链上数据索引器,而无需设置任何管道或基础设施。这使你能够更快地将产品推向市场,并更快速地进行数据驱动的迭代(在这一案例研究中,了解 Pendle 如何通过 Shadow 将其交易路由提高了 34%)。

Uniswap v3 shadow 事件

我们与 Uniswap 合作,为 Uniswap v3 池开发了 shadow 事件,这些事件解锁了更丰富的池活动和流动性动态数据集。

这个更丰富的数据集使我们能够:

  • 创建数据密集型图表,而无需进行任何 RPC 调用或使用任何外部 API
  • 对流动性提供者的行为和盈利能力进行复杂分析
  • 构建价格范围和时间内流动性分布的完整图景
  • 更深入理解内部池动态(例如:激活的 ticks、流动性范围、tick 费用累积)

但展示总比说明有效。因此,我们构建了一个仪表板——univ3.xyz,以展示这一独特 shadow 事件数据集解锁的一些内容。

我们已经为 USDC-WETH 5bps 池发布了这些数据,并鼓励研究人员和分析师分享反馈并进行自己的分析。

你可以在这篇文章的最后阅读关于每个 shadow 事件的详细解析。

图表说明

本节概述了 univ3.xyz 上每个仪表板图表所展示内容,以及如何使用 Uniswap v3 池 shadow 事件生成所呈现的数据。

💡 本版本的仪表板未包含诸如损失与 rebalancing(LVR) 无常损失(IL)等资本损失指标。我们希望未来能够添加这一类别的数据。

交易量与费用

条形图表示兑换交易量,线条表示收集的费用。

shadow 事件的使用方式:

  • ShadowSwap事件包含一个参数USDAmountE6,使用 ETH-USD Chainlink 价格预言机计算兑换的美元价值,具有区块级的准确性,无需使用外部链外价格 API。
  • 兑换的美元金额再乘以poolFee,来自ShadowSwap结构PoolInfo,其中还包含其他有用的池元数据,如poolNametickSpacing以及token0token1的地址、符号、名称和小数位数。

流动性与价格

在特定区块中,虚拟流动性在价格范围内的分布。底部的控制选项允许你浏览过去的区块范围。你也可以输入特定的区块编号。

shadow 事件的使用方式:

PoolLiquidityAtTickSpace shadow 事件在每次流动性mintburn发生时发出,并包含:

  • mintburn完成后,池在每个 tick-space 的更新总流动性
  • 当 tick 从左侧穿越到右侧时,新增的净流动性

这使我们能够在任何给定的区块高度生成池流动性分布的视图,而无需进行任何 RPC 调用。

LP 头寸

在特定区块中,跨越价格范围的最大流动性头寸。

shadow 事件的使用方式:

ShadowMintShadowBurn事件包含一个owner和一个sender参数,用于区分发起交易的 EOA(owner)和调用池函数的sender(通常是 Uniswap 的NonfungiblePositionManager合约)。

然后,基于ownertickLowertickUpper的组合生成positionId哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者关联的铸造和销毁进行映射,且不论流动性头寸是否通过NonfungiblePositionManager合约进行更改。

最后,我们在所选的区块高度使用最后一个ShadowSwap来计算每个流动性头寸的代币的美元价值。

流动性变化

在特定时间段内,池中的 token0/1 流动性变化。

shadow 事件的使用方式:

ShadowMintShadowBurn事件包含一个结构体TokenInfo,存储诸如tokenAddresstokenSymboltokenNametokenDecimals等元数据,这简化了图表中人类可读的代币数量的展现。

每个 tick 范围的费用

在特定时间段内,整个池在价格范围内赚取的费用。

shadow 事件的使用方式:

ShadowMintShadowBurn事件包含一个owner和一个sender参数,用于区分发起交易的 EOA(owner)和调用池函数的sender(通常是 Uniswap 的NonfungiblePositionManager合约)。

然后,基于ownertickLowertickUpper的组合生成positionId哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者关联的铸造和销毁进行映射,且不论流动性头寸是否通过NonfungiblePositionManager合约进行更改。

我们汇总了指定时间段内每个 tick 空间的总兑换量(以 USD 计),并乘以 poolFee 以显示每个 tick 累积的费用。

收取的费用

在给定时间段内,单个流动性头寸收取的费用。

如何使用 shadow 事件:

ShadowMintShadowBurn 事件包含一个 owner 和一个 sender 参数,这些参数用于区分发起交易的 EOA(owner)与调用池函数的 sender(通常是 Uniswap 的 NonfungiblePositionManager 合约)。

然后,从 ownertickLowertickUpper 的组合生成一个 positionId 哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者相关联的铸造和销毁进行映射,而无需考虑流动性头寸是否通过 NonfungiblePositionManager 合约进行了更改。

ShadowBurn 事件还包括 FeesEarned.token0FeesEarned.token1 参数,这将累积到流动性头寸的兑换费用从原始存入池的 token0token1 金额中分离出来。

按收取费用排序的前流动性提供者

在给定时间段内按收取费用排序的流动性提供者。

如何使用 shadow 事件:

ShadowMintShadowBurn 事件包含一个 owner 和一个 sender 参数,这些参数用于区分发起交易的 EOA(owner)与调用池函数的 sender(通常是 Uniswap 的 NonfungiblePositionManager 合约)。

然后,从 ownertickLowertickUpper 的组合生成一个 positionId 哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者相关联的铸造和销毁进行映射,而无需考虑流动性头寸是否通过 NonfungiblePositionManager 合约进行了更改。

ShadowBurn 事件包含 FeesEarned.token0FeesEarned.token1 参数,这将累积到流动性头寸的兑换费用从原始存入池的 token0token1 金额中分离出来。

数据说明

本节详细说明了每个 Uniswap v3 池合约中的 shadow 事件所包含的数据。我们假设你熟悉 Uniswap v3 的基本概念。如果你需要回顾或想了解相关内容,请阅读以下帖子:

💡 资本损失指标,如与再平衡相关的损失(LVR)和无常损失(IL)不包括在这一组 shadow 事件中。我们鼓励研究人员使用 Shadow 捕捉这些指标的数据!

符号表示法

由于 Solidity 不支持小数,我们使用 E 表示法来表示乘以 1eN 的值,以保持精度。当向用户呈现值时,除以 1eN

  • 例如:如果 USDAmountE6 = 1,245,480,000,则应将其除以 1e6 以得出可读的值 1,245.48

Uniswap 合约中的一些其他值,如 sqrtPriceX96 ,由 Q 表示法表示,你可以 在此处了解更多

结构体

我们广泛使用结构体以保持数据参数的组织,并避免 Solidity 编译器的 Stack too deep 错误。Shadow 移除了 gas 账目和合约代码大小限制,因此你可以添加尽可能多的数据日志。

💡 一些结构体在多个 shadow 事件中重复使用,这保持了命名的一致性,并减少了额外表连接的需求。

SwapInfo

    // 兑换信息的结构体
    struct SwapInfo {
        // 发起兑换调用的地址,并接收到回调
        address sender;
        // 接收兑换输出的地址
        address recipient;
        // 池的 token0 余额的变化量
        int256 amount0;
        // 池的 token1 余额的变化量
        int256 amount1;
        // 使用 Chainlink 预言机的兑换的 USD 金额,乘以 1e6 以保持后续存储精度
        uint256 USDAmountE6;
        // 兑换后池的 sqrt(price),表示为 Q64.96
        uint160 sqrtPriceX96;
        // 兑换后池的流动性
        uint128 liquidity;
        // 兑换后池价格的 1.0001 的对数
        int24 tick;
    }

TokenInfo

    // 一般代币信息的结构体
    struct TokenInfo {
        // 代币地址
        address tokenAddress;
        // 代币符号,例如 "WETH"
        string tokenSymbol;
        // 代币名称,例如 "Wrapped Ether"
        string tokenName;
        // 代币小数位
        uint8 tokenDecimals;
    }

PoolInfo

    // 一般池信息的结构体
    struct PoolInfo {
        // 池的 token0
        TokenInfo token0;
        // 池的 token1
        TokenInfo token1;
        // 池的费用,以百分之一的 bip 表示,即 1e-6
        uint24 poolFee;
        // 池的 Shadow 名称;格式为 "token0Symbol-token1Symbol poolFee bps"
        string poolName;
        // 池的 tick 间隔
        int24 tickSpacing;
    }

PositionDetails

    // 一般流动性头寸信息的结构体
    struct PositionDetails {
        // 流动性头寸的所有者;如果通过 NonfungiblePositionManager 铸造,则为 NonfungiblePositionManager 的 mint() 函数的 msg.sender
        address owner;
        // 在池合约上调用 mint() 函数的发送者;可能与所有者不同,如果使用则为 NonfungiblePositionManager
        address sender;
        // 头寸的下限 tick
        int24 tickLower;
        // 头寸的上限 tick
        int24 tickUpper;
        // 铸造到头寸范围的流动性数量
        uint128 amount;
        // 铸造流动性所需的 token0 数量
        uint256 amount0;
        // 铸造流动性所需的 token1 数量
        uint256 amount1;
        // 从 owner、tickLower 和 tickUpper 的 keccak256 哈希生成的 Shadow positionId
        bytes32 positionId;
    }

FeesEarned

    // 收取的费用的结构体,在 ShadowBurn 事件中使用
    struct FeesEarned {
        // token0 收取的费用
        uint128 token0;
        // token1 收取的费用
        uint128 token1;
    }

PositionFeeValues

    // 流动性头寸费用值的结构体;有助于计算头寸费用,在 ShadowMint 和 ShadowBurn 事件中使用
    struct PositionFeeValues {
        // feeGrowthGlobal0E18 token0 的全球费用增长,其中 feeGrowthGlobal0E18 = (feeGrowthGlobal0X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthGlobal0E18;
        // feeGrowthGlobal1E18 token1 的全球费用增长,其中 feeGrowthGlobal1E18 = (feeGrowthGlobal1X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthGlobal1E18;
        // token0 上限 tick 之外的费用增长,其中 feeGrowthOutsideUpper0E18 = (feeGrowthOutsideUpper0X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthOutsideUpper0E18;
        // token0 下限 tick 之外的费用增长,其中 feeGrowthOutsideLower0E18 = (feeGrowthOutsideLower0X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthOutsideLower0E18;
        // 在最后一次铸造/销毁/poke token0 时的费用增长范围内,其中 feeGrowthInside0LastE18 = (feeGrowthInside0LastX128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthInside0LastE18;
        // token1 上限 tick 之外的费用增长,其中 feeGrowthOutsideUpper1E18 = (feeGrowthOutsideUpper1X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthOutsideUpper1E18;
        // token1 下限 tick 之外的费用增长,其中 feeGrowthOutsideLower1E18 = (feeGrowthOutsideLower1X128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthOutsideLower1E18;
        // 在最后一次铸造/销毁/poke token1 时的费用增长范围内,其中 feeGrowthInside1LastE18 = (feeGrowthInside1LastX128 * 10**18) >> 128,以保持后续数据存储的精度
        uint256 feeGrowthInside1LastE18;
    }

LiquidityInRangeValues

    // 表示范围内流动性的结构体,用于 ShadowMint 和 ShadowBurn 事件
    struct LiquidityInRangeValues {
        // 该位置的范围内流动性
        uint128 positionLiquidityInRange;
        // 事件前的总范围内流动性
        uint128 totalLiquidityInRangeBefore;
        // 事件后的总范围内流动性
        uint128 totalLiquidityInRangeAfter;
    }

Shadow 事件

ShadowSwap

我们在原始的 Swap 事件基础上进行了扩展,以:

  • 添加 freeGrowthGlobal,使我们能够查看每个兑换如何贡献于池费用
  • 添加基于兑换时 Chainlink oracle 的 ETH-USD 价格的兑换 USDAmountE6
  • SwapInfoPoolInfo 结构体中添加有用的元数据,例如代币符号和代币小数位数。
    /// @notice 由池发布的任何 token0 和 token1 之间的兑换事件
    /// @param swapInfo 结构体,包含有关兑换的基本信息
    /// @param poolInfo 结构体,包含有关池的基本信息
    /// @param feeGrowthGlobal0E18 token0 的全局费用增长,其中 feeGrowthGlobal0E18 = (feeGrowthGlobal0X128 * 10**18) >> 128,以便进行下游数据存储并保持精度
    /// @param feeGrowthGlobal1E18 token1 的全局费用增长,其中 feeGrowthGlobal1E18 = (feeGrowthGlobal1X128 * 10**18) >> 128,以便进行下游数据存储并保持精度
    event ShadowSwap(
        SwapInfo swapInfo,
        PoolInfo poolInfo,
        uint256 feeGrowthGlobal0E18,
        uint256 feeGrowthGlobal1E18,
        int256 chainlinkETHUSDPriceE8
    );

TickCrossed

这个事件在原始合约中根本不存在。当一次兑换导致池价格移动足够以跨越到另一个价格刻度时,会发出该事件。如果兑换金额足够大,可以跨越多个刻度,因此会发出多个 TickCrossed 事件。

TickCrossed 事件包含了所有新跨越刻度的状态。这些数据对于在任何给定时间全面了解池的流动性状态至关重要,并且对分析流动性提供者的盈利能力特别有帮助。

    /// @notice 当跨越一个刻度时发出
    /// @param tick 被跨越的刻度的索引
    /// @param liquidityGross 参考此刻度的总位置流动性
    /// @param liquidityNet 跨越刻度时添加(减去)的净流动性,方向为从左到右(从右到左)
    /// @param feeGrowthOutside0E18 此刻度另一侧 token0 的每单位流动性的费用增长(相对于当前刻度)。仅具有相对意义,绝对无意义——该值取决于刻度何时初始化。 feeGrowthOutside0E18 = (feeGrowthOutside0X128 * 10**18) >> 128,以便进行下游数据存储并保持精度
    /// @param feeGrowthOutside1E18 此刻度另一侧 token1 的每单位流动性的费用增长(相对于当前刻度)。仅具有相对意义,绝对无意义——该值取决于刻度何时初始化。 feeGrowthOutside1E18 = (feeGrowthOutside1X128 * 10**18) >> 128,以便进行下游数据存储并保持精度
    /// @param tickCumulativeOutside 此刻度另一侧的累积刻度值
    /// @param secondsPerLiquidityOutsideX128 此刻度另一侧每单位流动性的秒数(相对于当前刻度)。仅具有相对意义,绝对无意义——此值取决于刻度何时初始化
    /// @param secondsOutside 在此刻度另一侧花费的秒数(相对于当前刻度)。仅具有相对意义,绝对无意义——此值取决于刻度何时初始化
    event TickCrossed(
        int24 indexed tick,
        uint128 liquidityGross,
        int128 liquidityNet,
        uint256 feeGrowthOutside0E18,
        uint256 feeGrowthOutside1E18,
        int56 tickCumulativeOutside,
        uint160 secondsPerLiquidityOutsideX128,
        uint32 secondsOutside
    );

ShadowMint

我们在原始的 Mint 事件基础上进行了扩展,以:

  • PositionFeeValues 结构体中添加 feeGrowthGlobalfeeGrowthOutsidefeeGrowthInsideLast 等值,这使我们能够计算 未收取的费用
  • LiquidityInRangeValues 结构体中添加关于范围内与范围外流动性的值
  • 添加一些关于位置的有用元数据,以及区块中的 Chainlink oracle ETH-USD 价格

ShadowMint 事件中包含的附加值对于分析流动性提供者的盈利能力以及驱动他们行为和策略的重要性。

    /// @notice 当为给定位置铸造流动性时发出
    /// @param positionDetails 结构体,包含有关所有者、发送者、刻度范围、流动性和代币数量以及 Shadow positionId 的信息
    /// @param positionFeeValues 结构体,包含有关 token0 和 token1 的全局费用增长、外部费用增长和内部费用增长的信息,有助于计算头寸费用
    /// @param liquidityInRangeValues 结构体,包含有关位置范围内流动性的信息,以及事件前后的总范围内流动性
    /// @param poolInfo 结构体,包含有关池的基本信息
    /// @param tick 铸造后池价格的以 1.0001 为底的对数
    event ShadowMint(
        PositionDetails positionDetails,
        PositionFeeValues positionFeeValues,
        LiquidityInRangeValues liquidityInRangeValues,
        PoolInfo poolInfo,
        int24 tick,
        int256 chainlinkETHUSDPriceE8
    );

ShadowBurn

我们在原始的 Burn 事件基础上进行了扩展,以:

  • PositionFeeValues 结构体中添加 feeGrowthGlobalfeeGrowthOutsidefeeGrowthInsideLast 等值,这使我们能够计算 未收取的费用
  • 添加该头寸获得的费用,这在原始合约的 Burn 事件中并没有详细信息
  • LiquidityInRangeValues 结构体中添加关于范围内与范围外流动性的值
  • 添加一些关于位置的有用元数据,以及区块中的 Chainlink oracle ETH-USD 价格

ShadowBurn 事件中包含的附加值对于分析流动性提供者的盈利能力,以及驱动他们行为和策略的重要性。

    /// @notice 当移除头寸的流动性时发出
    /// @dev 不会提取流动性头寸所赚取的任何费用,必须通过 #collect 提取
    /// @param positionDetails 结构体,包含有关所有者、发送者、刻度范围、流动性和代币数量以及 Shadow positionId 的信息
    /// @param feesEarned 结构体,包含有关头寸赚取的费用的信息;从 amount0 和 amount1 中减去 feesEarned 以计算用于铸造头寸的 token0 和 token1
    /// @param positionFeeValues 结构体,包含有关 token0 和 token1 的全局费用增长、外部费用增长和内部费用增长的信息,有助于计算头寸费用
    /// @param liquidityInRangeValues 结构体,包含有关头寸范围内流动性的信息,以及事件前后的总范围内流动性
    /// @param poolInfo 结构体,包含有关池的基本信息
    /// @param tick 烧掉后池价格的以 1.0001 为底的对数
    event ShadowBurn(
        PositionDetails positionDetails,
        FeesEarned feesEarned,
        PositionFeeValues positionFeeValues,
        LiquidityInRangeValues liquidityInRangeValues,
        PoolInfo poolInfo,
        int24 tick,
        int256 chainlinkETHUSDPriceE8
    );

ShadowCollect

我们扩展了原始的 Collect 事件以:

  • 添加区块中 Chainlink 预言机的 ETH-USD 价格
    /// @notice 当头寸的所有者收取费用时发出
    /// @dev 当调用者选择不收取费用时,收集事件可能会发出零 amount0 和 amount1
    /// @param owner 收取费用的头寸所有者;如果通过 NonfungiblePositionManager 调用,将是 NonfungiblePositionManager 的 msg.sender
    /// @param recipient 收取的费用的接收者
    /// @param tickLower 头寸的下限 tick
    /// @param tickUpper 头寸的上限 tick
    /// @param amount0 收取的 token0 费用数量
    /// @param amount1 收取的 token1 费用数量
    event ShadowCollect(
        address indexed owner,
        address recipient,
        int24 indexed tickLower,
        int24 indexed tickUpper,
        uint128 amount0,
        uint128 amount1,
        int256 chainlinkETHUSDPriceE8
    );

PoolLiquidityAtTickSpace

此事件在原始合约中完全不存在。当发生流动性 mintburn 时,会发出该事件,并包含:

  • 流动性头寸的每个 tick-space 的更新总流动性,在 mintburn 完成后
  • 当 tick 从左到右跨越时更新的净流动性数量

对于每个流动性头寸的 mintburn,至少会发出一个 PoolLiquidityAtTickSpace 事件,并且大多数情况下,一个流动性头寸的 mintburn 会发出多个 PoolLiquidityAtTickSpace 事件,因为大多数流动性头寸跨越多个 tick-spaces。

PoolLiquidityAtTickSpace 事件对于生成任意给定区块高度的池流动性分布图至关重要。

    /// @notice 在流动性铸造和销毁时发出;包括头寸每个 tick 空间的池流动性
    /// @param tickSpaceLower tick 空间的下界;每个 tick 空间的大小为 tickSpacing(例如,如果 tickSpacing = 10,则 tickSpaceLower 为 201250 的 tick 空间跨度为 [201250, 201260))
    /// @param poolLiquidityAtTickSpace 如果活动 tick 位于 [tickSpaceLower, tickSpaceLower + tickSpacing) 内,池的流动性
    /// @param liquidityNet 当 tick 从左到右(右到左)跨越时添加(减去)的净流动性数量
    /// @param poolInfo 有关池的基本信息的结构
    /// @param positionId 从 owner、tickLower 和 tickUpper 的 keccak256 哈希生成的 Shadow positionId
    event PoolLiquidityAtTickSpace(
        int24 tickSpaceLower,
        uint128 poolLiquidityAtTickSpace,
        int128 liquidityNet,
        PoolInfo poolInfo,
        bytes32 positionId
    );

结论

Shadow 赋予你将离线事件日志添加到任何已部署智能合约的超能力,通过一个易于使用的托管平台,消除了对复杂数据管道和其他节点基础设施的需求。

Shadow 事件带来了明显的好处:

  1. 更深入的数据覆盖:通过访问以前无法访问(或非常难以访问)的链上数据,生成全新的事件,针对任何智能合约。
  2. 简化的数据管道:通过直接在智能合约中编写转换逻辑,大幅降低数据管道的复杂性。
  3. 更快的迭代周期:使用你已经熟悉的工具快速测试、验证和迭代 Shadow 事件。
  4. 无权限、无 gas 日志记录:无权限地在你想要的任何合约上添加任意多的事件,而不会增加最终用户的 gas 负担。

有了 Shadow,一个人可以在一个下午构建一个强大的链上数据索引器,而无需设置任何管道或基础设施。这使你能够更快地将产品推向市场,并更快地进行基于数据的迭代(请参见 Pendle 如何通过 Shadow 将其交易路由改进 34% 的 案例研究)。

我们建立了 一个仪表盘,利用这个数据集和我们最近推出的 实时数据库同步 来展示你可以用 Shadow 构建的内容。

感谢 Austin AdamsDan Robinson 在这些 Shadow 事件上的合作,感谢 Achal 设计仪表盘,以及 Ciamac MoallemisaucepointAlex NezlobinStorm 和其他人对 univ3.xyz 的反馈。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Shadow
Shadow
只需几行代码,即可开启链上数据的新世界。 https://www.shadow.xyz/