使用 shadow 构建强大的 Uniswap V3 数据面板
- 原文链接:learnblockchain.cn/uniswap-v3
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
Shadow 与 Uniswap 合作,为 Uniswap v3 池开发了自定义的 shadow 事件,从而解锁了更丰富的池活动和流动性动态数据集。
我们构建了一个仪表板(univ3.xyz),利用这个数据集和我们最近推出的实时数据库同步 ,展示你可以用 Shadow 构建的内容。
在这篇文章中,我们将详细介绍这些 Uniswap v3 shadow 事件,它们解锁的新数据,以及它们是如何用来生成每个仪表板图表的。
Shadow 赋予你将链外事件日志添加到任何部署的智能合约的超级能力,并提供一个易于使用的托管平台,消除复杂数据管道和其他节点基础设施的需求。
Shadow 为顶级加密工程和数据团队节省了宝贵的时间——链上数据问题曾经需要数周解决,现在缩短为几小时甚至几分钟。
借助 Shadow,一个人可以在一个下午构建一个强大的链上数据索引器,而无需设置任何管道或基础设施。这使你能够更快地将产品推向市场,并更快速地进行数据驱动的迭代(在这一案例研究中,了解 Pendle 如何通过 Shadow 将其交易路由提高了 34%)。
我们与 Uniswap 合作,为 Uniswap v3 池开发了 shadow 事件,这些事件解锁了更丰富的池活动和流动性动态数据集。
这个更丰富的数据集使我们能够:
但展示总比说明有效。因此,我们构建了一个仪表板——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
,其中还包含其他有用的池元数据,如poolName
、tickSpacing
以及token0
和token1
的地址、符号、名称和小数位数。在特定区块中,虚拟流动性在价格范围内的分布。底部的控制选项允许你浏览过去的区块范围。你也可以输入特定的区块编号。
shadow 事件的使用方式:
PoolLiquidityAtTickSpace
shadow 事件在每次流动性mint
或burn
发生时发出,并包含:
mint
或burn
完成后,池在每个 tick-space 的更新总流动性这使我们能够在任何给定的区块高度生成池流动性分布的视图,而无需进行任何 RPC 调用。
在特定区块中,跨越价格范围的最大流动性头寸。
shadow 事件的使用方式:
ShadowMint
和ShadowBurn
事件包含一个owner
和一个sender
参数,用于区分发起交易的 EOA(owner
)和调用池函数的sender
(通常是 Uniswap 的NonfungiblePositionManager
合约)。
然后,基于owner
、tickLower
和tickUpper
的组合生成positionId
哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者关联的铸造和销毁进行映射,且不论流动性头寸是否通过NonfungiblePositionManager
合约进行更改。
最后,我们在所选的区块高度使用最后一个ShadowSwap
来计算每个流动性头寸的代币的美元价值。
在特定时间段内,池中的 token0/1 流动性变化。
shadow 事件的使用方式:
ShadowMint
和ShadowBurn
事件包含一个结构体TokenInfo
,存储诸如tokenAddress
、tokenSymbol
、tokenName
和tokenDecimals
等元数据,这简化了图表中人类可读的代币数量的展现。
在特定时间段内,整个池在价格范围内赚取的费用。
shadow 事件的使用方式:
ShadowMint
和ShadowBurn
事件包含一个owner
和一个sender
参数,用于区分发起交易的 EOA(owner
)和调用池函数的sender
(通常是 Uniswap 的NonfungiblePositionManager
合约)。
然后,基于owner
、tickLower
和tickUpper
的组合生成positionId
哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者关联的铸造和销毁进行映射,且不论流动性头寸是否通过NonfungiblePositionManager
合约进行更改。
我们汇总了指定时间段内每个 tick 空间的总兑换量(以 USD 计),并乘以 poolFee
以显示每个 tick 累积的费用。
在给定时间段内,单个流动性头寸收取的费用。
如何使用 shadow 事件:
ShadowMint
和 ShadowBurn
事件包含一个 owner
和一个 sender
参数,这些参数用于区分发起交易的 EOA(owner
)与调用池函数的 sender
(通常是 Uniswap 的 NonfungiblePositionManager
合约)。
然后,从 owner
、tickLower
和 tickUpper
的组合生成一个 positionId
哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者相关联的铸造和销毁进行映射,而无需考虑流动性头寸是否通过 NonfungiblePositionManager
合约进行了更改。
ShadowBurn
事件还包括 FeesEarned.token0
和 FeesEarned.token1
参数,这将累积到流动性头寸的兑换费用从原始存入池的 token0
和 token1
金额中分离出来。
在给定时间段内按收取费用排序的流动性提供者。
如何使用 shadow 事件:
ShadowMint
和 ShadowBurn
事件包含一个 owner
和一个 sender
参数,这些参数用于区分发起交易的 EOA(owner
)与调用池函数的 sender
(通常是 Uniswap 的 NonfungiblePositionManager
合约)。
然后,从 owner
、tickLower
和 tickUpper
的组合生成一个 positionId
哈希。这使我们能够在不进行 RPC 调用的情况下,将与单个所有者相关联的铸造和销毁进行映射,而无需考虑流动性头寸是否通过 NonfungiblePositionManager
合约进行了更改。
ShadowBurn
事件包含 FeesEarned.token0
和 FeesEarned.token1
参数,这将累积到流动性头寸的兑换费用从原始存入池的 token0
和 token1
金额中分离出来。
本节详细说明了每个 Uniswap v3 池合约中的 shadow 事件所包含的数据。我们假设你熟悉 Uniswap v3 的基本概念。如果你需要回顾或想了解相关内容,请阅读以下帖子:
💡 资本损失指标,如与再平衡相关的损失(LVR)和无常损失(IL)不包括在这一组 shadow 事件中。我们鼓励研究人员使用 Shadow 捕捉这些指标的数据!
由于 Solidity 不支持小数,我们使用 E
表示法来表示乘以 1eN
的值,以保持精度。当向用户呈现值时,除以 1eN
。
USDAmountE6
= 1,245,480,000,则应将其除以 1e6
以得出可读的值 1,245.48Uniswap 合约中的一些其他值,如 sqrtPriceX96
,由 Q 表示法表示,你可以 在此处了解更多。
我们广泛使用结构体以保持数据参数的组织,并避免 Solidity 编译器的 Stack too deep
错误。Shadow 移除了 gas 账目和合约代码大小限制,因此你可以添加尽可能多的数据日志。
💡 一些结构体在多个 shadow 事件中重复使用,这保持了命名的一致性,并减少了额外表连接的需求。
// 兑换信息的结构体
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;
}
// 一般代币信息的结构体
struct TokenInfo {
// 代币地址
address tokenAddress;
// 代币符号,例如 "WETH"
string tokenSymbol;
// 代币名称,例如 "Wrapped Ether"
string tokenName;
// 代币小数位
uint8 tokenDecimals;
}
// 一般池信息的结构体
struct PoolInfo {
// 池的 token0
TokenInfo token0;
// 池的 token1
TokenInfo token1;
// 池的费用,以百分之一的 bip 表示,即 1e-6
uint24 poolFee;
// 池的 Shadow 名称;格式为 "token0Symbol-token1Symbol poolFee bps"
string poolName;
// 池的 tick 间隔
int24 tickSpacing;
}
// 一般流动性头寸信息的结构体
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;
}
// 收取的费用的结构体,在 ShadowBurn 事件中使用
struct FeesEarned {
// token0 收取的费用
uint128 token0;
// token1 收取的费用
uint128 token1;
}
// 流动性头寸费用值的结构体;有助于计算头寸费用,在 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;
}
// 表示范围内流动性的结构体,用于 ShadowMint 和 ShadowBurn 事件
struct LiquidityInRangeValues {
// 该位置的范围内流动性
uint128 positionLiquidityInRange;
// 事件前的总范围内流动性
uint128 totalLiquidityInRangeBefore;
// 事件后的总范围内流动性
uint128 totalLiquidityInRangeAfter;
}
我们在原始的 Swap
事件基础上进行了扩展,以:
freeGrowthGlobal
,使我们能够查看每个兑换如何贡献于池费用USDAmountE6
SwapInfo
和 PoolInfo
结构体中添加有用的元数据,例如代币符号和代币小数位数。 /// @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
事件包含了所有新跨越刻度的状态。这些数据对于在任何给定时间全面了解池的流动性状态至关重要,并且对分析流动性提供者的盈利能力特别有帮助。
/// @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
);
我们在原始的 Mint
事件基础上进行了扩展,以:
PositionFeeValues
结构体中添加 feeGrowthGlobal
、feeGrowthOutside
和 feeGrowthInsideLast
等值,这使我们能够计算 未收取的费用LiquidityInRangeValues
结构体中添加关于范围内与范围外流动性的值在 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
);
我们在原始的 Burn
事件基础上进行了扩展,以:
PositionFeeValues
结构体中添加 feeGrowthGlobal
、feeGrowthOutside
和 feeGrowthInsideLast
等值,这使我们能够计算 未收取的费用Burn
事件中并没有详细信息LiquidityInRangeValues
结构体中添加关于范围内与范围外流动性的值在 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
);
我们扩展了原始的 Collect
事件以:
/// @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
);
此事件在原始合约中完全不存在。当发生流动性 mint
或 burn
时,会发出该事件,并包含:
mint
或 burn
完成后对于每个流动性头寸的 mint
或 burn
,至少会发出一个 PoolLiquidityAtTickSpace
事件,并且大多数情况下,一个流动性头寸的 mint
或 burn
会发出多个 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 事件带来了明显的好处:
有了 Shadow,一个人可以在一个下午构建一个强大的链上数据索引器,而无需设置任何管道或基础设施。这使你能够更快地将产品推向市场,并更快地进行基于数据的迭代(请参见 Pendle 如何通过 Shadow 将其交易路由改进 34% 的 案例研究)。
我们建立了 一个仪表盘,利用这个数据集和我们最近推出的 实时数据库同步 来展示你可以用 Shadow 构建的内容。
感谢 Austin Adams 和 Dan Robinson 在这些 Shadow 事件上的合作,感谢 Achal 设计仪表盘,以及 Ciamac Moallemi、saucepoint、Alex Nezlobin、Storm 和其他人对 univ3.xyz 的反馈。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!