BlockVectra

WETH 封装代币流水数据集(`weth_flows` & `daily_weth_net_wrap`)

面向直接写 SQL 的内部团队。覆盖 Robinhood Chain(chain id 4663,Arbitrum Orbit)上原生代币封装/解封装(wrap / unwrap)流水与每日净封装量统计。 对应 DDL 与物化视图定义在 schema/clickhouse/derived/025_weth_…

This content is sourced from upstream and is currently available in Chinese only.

面向直接写 SQL 的内部团队。覆盖 Robinhood Chain(chain id 4663,Arbitrum Orbit)上原生代币封装/解封装(wrap / unwrap)流水与每日净封装量统计。 对应 DDL 与物化视图定义在 schema/clickhouse/derived/025_weth_flows.sql。库名以 robinhood 为例。

0. 核心结论与链上 WETH 合约调研

在 Robinhood Chain 上,关于 WETH(Wrapped Ether)存在两类不同架构的合约:

1. Arbitrum 官方代币网关的跨链 WETH (aeWETH)

  • 合约地址:0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73(代理合约,逻辑实现合约为 0xC6B81b429797e0f555440B70CD99E032D7Ae947E)。
  • 链上元数据:
    • name() = "WETH"
    • symbol() = "WETH"
    • decimals() = 18
    • l1Address() = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2(Ethereum 主网 Canonical WETH9)
    • l2Gateway() = 0x1D187C3E2dA52D72BC9C41e3AbA0fdFa6a7bF055(L2 WETH Gateway)
    • totalSupply() = 36,720,160,610,959,967,162,139 (~36,720 WETH)
  • 机制与调研特征: 这是 Robinhood 官方生态的主力 WETH(在 docs/datasets/labels.md 中标记为 WETH,生产环境 Transfer 事件超过 2.37 亿笔)。然而,该合约是典型的 Arbitrum L2 跨链桥代币,其供应量的铸造和销毁完全由 L2 跨链网关调用 bridgeMint 与 bridgeBurn 完成(仅发出 Transfer(address(0), ...) / Transfer(..., address(0)) 日志)。深入反编译与分析其实现合约字节码表明,该合约不包含 WETH9 的 Deposit 或 Withdrawal 事件哈希。在生产环境对全量 7200 万区块的日志扫描中,该合约发出的 Deposit(address,uint256) 与 Withdrawal(address,uint256) 事件计数为 0。

2. 独立部署的原生 WETH9 克隆合约(支持 Wrap / Unwrap)

在生产环境扫描全量日志中符合标准 WETH9 签名定义的事件:

  • Deposit(address indexed dst, uint256 wad) topic0 = keccak256("Deposit(address,uint256)") = 0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
  • Withdrawal(address indexed src, uint256 wad) topic0 = keccak256("Withdrawal(address,uint256)") = 0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65

通过对所有发出上述 topic0 的候选合约发起 eth_call 探测(验证 name()、symbol()、decimals()、l1Address()),排除了发出同名事件但非 ERC20 的路由与金库合约(例如 1inch 路由 0x1111110f0f73c0b2ef09ec012eae758b3e03a902、1inch v6 路由 0x1111115af63bfbcc151e5753e2ea1a29c79d2f01、未知流动性金库 0x4f82e73edb06d29ff62c91ec8f5ff06571bdeb29 等均无法响应 ERC20 方法调用),最终严格筛选出 4 个纯正的 WETH9 克隆合约:

合约地址起始区块截止区块 / 状态生产 Deposit 笔数生产 Withdrawal 笔数链上元数据 (eth_call)
0xa687d76e7e5fd469cce7dca4f7d2b767fe7737ae933,6433,272,619 (早期)461"Wrapped Ether" / WETH / 18
0xabbd2fc6065f0ac5929500f5326db0c37d186be18,666,6588,702,437 (短期)51"Wrapped Ether" / WETH / 18
0xa753e1be2d3a068218aca5f1ca85042c046915dc13,065,47019,966,937 (中期)3823"Wrapped Ether" / WETH / 18
0xc08751e47611f035b958889557edbbe33d4a8bce9,561,441当前链端 (~72.06M 活跃)8388"Wrapped Ether" / WETH / 18

这 4 个合约均属于经典的 WETH9 实现:

  • 用户转入原生 ETH 即触发 Deposit(dst, wad) 铸造等量 WETH(wrap);
  • 用户调用 withdraw(wad) 销毁 WETH 并赎回等量原生 ETH(unwrap),触发 Withdrawal(src, wad);
  • 两个事件均满足 LOG2 结构:topic1 存储用户地址,topic2/topic3 为空,data 精确为 32 字节 uint256 金额。
  • 0xc08751e47611f035b958889557edbbe33d4a8bce 是当前活跃度最高、持续服务于主网的 WETH9 存取合约。

物化视图采用严格地址白名单机制(包含上述 4 个确认为 WETH9 的地址,以及官方网关 WETH 0x0bd7d3...),杜绝全表扫描 topic0 导致的非 WETH 金库伪事件污染。

1. 表结构与字段设计

1.1 {db}.weth_flows(明细表)

记录单笔 wrap / unwrap 动作,与 {db}.logs 1:1 映射。

字段类型含义与说明
tokenFixedString(20)WETH 合约地址(raw bytes,查询时使用 hex(token) 显示)
accountFixedString(20)存入/提取人地址(由 topic1 提取去除 12 字节前导零,保留 20 字节)
directionEnum8('wrap' = 1, 'unwrap' = 2)动作方向:wrap(存入 ETH 得到 WETH)或 unwrap(销毁 WETH 取回 ETH)
amountUInt256发生金额(单位:wei,无损精确存储,禁止浮点数)
block_numberUInt64发生区块号
block_timestampDateTime('UTC')区块时间戳(UTC)
tx_hashFixedString(32)交易哈希(raw bytes)
tx_indexUInt32交易在该区块内的索引
log_indexUInt32日志在该区块内的全局索引
versionUInt64继承自原始 log,供 ReplacingMergeTree 去重
is_deletedUInt8继承自原始 log,供 reorg 墓碑折叠
  • 主键与排序键:ORDER BY (token, block_number, log_index)。针对“按 token 查询存取流水”与“区块区间扫描”进行物理聚簇。
  • 跳数索引:INDEX idx_account account TYPE bloom_filter GRANULARITY 4,加速按特定用户地址过滤流水。

1.2 {db}.daily_weth_net_wrap(每日净封装统计)

提供每个 WETH 代币在已闭合自然日(UTC)的汇总存取量与净流向数据。

字段类型含义与说明
dayDateUTC 日期
tokenFixedString(20)WETH 合约地址
wrap_amountUInt256当日累计 wrap 存入金额(wei)
unwrap_amountUInt256当日累计 unwrap 取出金额(wei)
net_wrap_amountInt256当日净 wrap 金额(wrap_amount - unwrap_amount,带符号 Int256)
wrap_countUInt64当日 wrap 笔数
unwrap_countUInt64当日 unwrap 笔数
refreshed_atDateTime('UTC')视图最近一次刷新时间
  • 主键与排序键:ORDER BY (day, token)。
  • 引擎:MergeTree。每次刷新原子重算并全量替换目标表,天然杜绝重复版本积累与重组残留。

2. 正确性原则与防双计设计(Correctness & Money Safety)

2.1 为什么明细表使用 1:1 的 Insert-Trigger 物化视图(模式 a)

weth_flows 的每行与源表 {db}.logs 严格 1:1 映射,排序键包含唯一的 (token, block_number, log_index),并直接透传 version 与 is_deleted。 当原始日志发生断点续传(backfill resume 插入的同版本重复行)或发生链重组(reorg 写入 is_deleted = 1 且版本更高的墓碑行)时,{db}.weth_flows FINAL WHERE is_deleted = 0 的去重行为与底层 {db}.logs FINAL 完全同构,明细表天然免疫双计问题。

2.2 为什么每日聚合表采用不带 APPEND 的全量原子替换可刷新视图

与每天产生数百万条记录的高频表不同,WETH 存取在 Robinhood Chain 上是典型的低频事件(全网累计仅 ~171 笔,平均每天仅数笔)。在此类低频场景下,若采用类似高频表的 APPEND 增量追加 + 3 天滑动窗口模式,存在致命缺陷:

  • 重组清空分组导致的幻影行(Phantom Row):如果某天某 token 仅有 1 笔事件,重组发生后写入墓碑,weth_flows FINAL 变为空;此时带有 GROUP BY day, token 的增量刷新不会输出任何该分组的行。由于没有新行追加,ReplacingMergeTree 永远无法覆盖或收回之前的聚合行,导致幻影行永久残留于表中;
  • 滑动窗口外数据冻结:3 天窗口外的历史重组或断点修复无法被定时刷新覆盖。

由于 weth_flows 全表数据量极小(历史总共仅几百行,全量聚合耗时通常 < 10ms),mv_daily_weth_net_wrap 去除了 APPEND 关键字和 3 天窗口限制:

  • 每次刷新原子替换整个 daily_weth_net_wrap 表;
  • 直接基于 {db}.weth_flows FINAL WHERE is_deleted = 0 AND toDate(block_timestamp) < today() 对所有已闭合历史天进行完整重算;
  • 若某个分组因重组而被完全抹去,下一次刷新时该分组直接在表中消失,彻底杜绝幻影行;
  • 目标表使用简洁高效的 MergeTree 引擎,无需 ReplacingMergeTree 的版本折叠开销,也不需要单独的历史回填 INSERT。

2.3 金额类型安全与负数净额

  • 金额字段(amount, wrap_amount, unwrap_amount)始终使用 UInt256 存储精确 wei 值,禁止在 SQL 运算和存储中使用任何浮点类型(Float32/Float64)。
  • 净封装额 net_wrap_amount 必须为带符号的 Int256。当某日解封装量大于封装量时,该值为合法的负整数,绝对不会发生无符号整数下溢环绕(underflow)。
  • 展示层换算使用定点数 toDecimal128 除以 1000000000000000000,保留正负号与完整精度,严禁使用 formatReadableQuantity 进行浮点截断。

3. 本地端到端验证结果(100% 精确匹配)

所有测试在本地独立 ClickHouse 数据库 scratch_weth_wt51(包含 9,705,000 ~ 9,709,999 共 5,000 个真实区块数据)及远程 RPC 节点上实测执行:

3.1 RPC 原始日志 vs. ClickHouse 物化视图独立对比

在区块区间 [9705000, 9709999] 内,针对当前主 WETH 合约 0xc08751e47611f035b958889557edbbe33d4a8bce 进行比对:

  • RPC eth_getLogs 返回原始事件:
    • Wrap 事件(Deposit):4 笔
    • Unwrap 事件(Withdrawal):7 笔
    • 累计 wrap 金额:102,101,599,904,905,401 wei
    • 累计 unwrap 金额:1,674,995,512,882,075,429 wei
    • 净 wrap 金额:-1,572,893,912,977,170,028 wei
  • ClickHouse weth_flows FINAL 与 daily_weth_net_wrap 统计:
    • Wrap 笔数:4 笔
    • Unwrap 笔数:7 笔
    • wrap_amount:102101599904905401
    • unwrap_amount:1674995512882075429
    • net_wrap_amount:-1572893912977170028
    • 比对结果:100% 逐字逐位精确匹配(Exact Match)!

3.2 链上样本哈希与事件详情

方向区块号日志索引交易哈希 (tx_hash)用户地址 (account)金额 (wei)
unwrap9,705,588710xd9b7375b9a1b8c3682287f3296ad226397479936fd7c9f428b959a2179d75aaa0x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc426,741,382,627,020,569
wrap9,708,69170x412eeb7350baa20791af9a9bedde52f9c3291af03c2a74efd9d911a8f0718b970x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc10,000,000,000,000,000
wrap9,709,87630xf6c41c8ee03913267402b5eb244ee800e38d0fb9d1a995ffa709bcf36249f0550x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc71,896,057,984,569,859

3.3 重组(Reorg)与墓碑测试

向本地表注入测试日志(区块 9999999):

  1. 插入版本 1(is_deleted = 0),查询 FINAL 结果显示行数由 11 变为 12;
  2. 注入模拟重组墓碑(版本 2,is_deleted = 1),查询 FINAL WHERE is_deleted = 0 结果准确回退至 11 行,测试注入数据被彻底过滤且无任何脏状态残留。

3.4 重组分组清空与视图自动刷新测试

在本地测试中,向某一历史天注入唯一的单笔事件,执行 SYSTEM REFRESH VIEW 后目标表包含该分组(数量 1,净额 -7);随后对该事件注入重组墓碑(版本 2,is_deleted = 1),再次执行 SYSTEM REFRESH VIEW,目标表自动原子刷新,该 (day, token) 分组被彻底清除(行数为 0),验证了无幻影行残留。

4. 安装与补算

4.1 安装步骤

# 1. 创建明细表、全量可刷新聚合表及对应的物化视图
sed 's/{db}/robinhood/g' schema/clickhouse/derived/025_weth_flows.sql \
  | clickhouse-client --multiquery

4.2 历史数据补算(Backfill)

025_weth_flows.sql 中自带了幂等的回填 SQL 语句。若因维护需要单独补算历史:

-- 补算 weth_flows 明细(幂等执行,FINAL 会折叠同版本重复行):
INSERT INTO robinhood.weth_flows
SELECT
    address AS token,
    CAST(substring(assumeNotNull(topic1), 13, 20) AS FixedString(20)) AS account,
    multiIf(
        topic0 = unhex('e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c'), 'wrap',
        'unwrap'
    ) AS direction,
    reinterpretAsUInt256(reverse(data)) AS amount,
    block_number,
    block_timestamp,
    tx_hash,
    tx_index,
    log_index,
    version,
    is_deleted
FROM robinhood.logs
WHERE address IN (
        unhex('a687d76e7e5fd469cce7dca4f7d2b767fe7737ae'),
        unhex('abbd2fc6065f0ac5929500f5326db0c37d186be1'),
        unhex('a753e1be2d3a068218aca5f1ca85042c046915dc'),
        unhex('c08751e47611f035b958889557edbbe33d4a8bce'),
        unhex('0bd7d308f8e1639fab988df18a8011f41eacad73')
      )
  AND topic0 IN (
        unhex('e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c'),
        unhex('7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65')
      )
  AND topic1 IS NOT NULL
  AND topic2 IS NULL
  AND topic3 IS NULL
  AND length(data) = 32;

-- daily_weth_net_wrap 不需要手动单独补算:
-- mv_daily_weth_net_wrap 采用不带 APPEND 的全量原子替换机制,
-- 视图创建时会立即自动对所有历史已闭合天进行全量重算。
-- 如需手动立即触发最新全量刷新:
SYSTEM REFRESH VIEW robinhood.mv_daily_weth_net_wrap;

5. 常用查询示例

5.1 查询指定地址近期的 Wrap / Unwrap 流水

SELECT
    block_timestamp,
    direction,
    -- 格式化为常规 ETH 显示(18 位小数,使用 Decimal128 无损转换,禁止浮点数)
    toDecimal128(amount, 18) / 1000000000000000000 AS eth_amount,
    amount AS raw_wei,
    hex(tx_hash) AS tx_hash_hex
FROM robinhood.weth_flows FINAL
WHERE account = unhex('9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc')
  AND is_deleted = 0
ORDER BY block_number DESC, log_index DESC
LIMIT 10;

5.2 查询近期每日净 Wrap 趋势

SELECT
    day,
    hex(token) AS token_hex,
    wrap_count,
    unwrap_count,
    -- 正数表示净存入(净封装),负数表示净取出(净解封装)
    net_wrap_amount,
    toDecimal128(net_wrap_amount, 18) / 1000000000000000000 AS net_eth_amount
FROM robinhood.daily_weth_net_wrap
ORDER BY day DESC, token
LIMIT 30;

On this page