WETH 封装代币流水数据集(`weth_flows` & `daily_weth_net_wrap`)
面向直接写 SQL 的内部团队。覆盖 Robinhood Chain(chain id 4663,Arbitrum Orbit)上原生代币封装/解封装(wrap / unwrap)流水与每日净封装量统计。 对应 DDL 与物化视图定义在 schema/clickhouse/derived/025_weth_…
面向直接写 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()= 18l1Address()=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)") = 0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109cWithdrawal(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) |
|---|---|---|---|---|---|
0xa687d76e7e5fd469cce7dca4f7d2b767fe7737ae | 933,643 | 3,272,619 (早期) | 46 | 1 | "Wrapped Ether" / WETH / 18 |
0xabbd2fc6065f0ac5929500f5326db0c37d186be1 | 8,666,658 | 8,702,437 (短期) | 5 | 1 | "Wrapped Ether" / WETH / 18 |
0xa753e1be2d3a068218aca5f1ca85042c046915dc | 13,065,470 | 19,966,937 (中期) | 38 | 23 | "Wrapped Ether" / WETH / 18 |
0xc08751e47611f035b958889557edbbe33d4a8bce | 9,561,441 | 当前链端 (~72.06M 活跃) | 83 | 88 | "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 映射。
| 字段 | 类型 | 含义与说明 |
|---|---|---|
token | FixedString(20) | WETH 合约地址(raw bytes,查询时使用 hex(token) 显示) |
account | FixedString(20) | 存入/提取人地址(由 topic1 提取去除 12 字节前导零,保留 20 字节) |
direction | Enum8('wrap' = 1, 'unwrap' = 2) | 动作方向:wrap(存入 ETH 得到 WETH)或 unwrap(销毁 WETH 取回 ETH) |
amount | UInt256 | 发生金额(单位:wei,无损精确存储,禁止浮点数) |
block_number | UInt64 | 发生区块号 |
block_timestamp | DateTime('UTC') | 区块时间戳(UTC) |
tx_hash | FixedString(32) | 交易哈希(raw bytes) |
tx_index | UInt32 | 交易在该区块内的索引 |
log_index | UInt32 | 日志在该区块内的全局索引 |
version | UInt64 | 继承自原始 log,供 ReplacingMergeTree 去重 |
is_deleted | UInt8 | 继承自原始 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)的汇总存取量与净流向数据。
| 字段 | 类型 | 含义与说明 |
|---|---|---|
day | Date | UTC 日期 |
token | FixedString(20) | WETH 合约地址 |
wrap_amount | UInt256 | 当日累计 wrap 存入金额(wei) |
unwrap_amount | UInt256 | 当日累计 unwrap 取出金额(wei) |
net_wrap_amount | Int256 | 当日净 wrap 金额(wrap_amount - unwrap_amount,带符号 Int256) |
wrap_count | UInt64 | 当日 wrap 笔数 |
unwrap_count | UInt64 | 当日 unwrap 笔数 |
refreshed_at | DateTime('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,401wei - 累计 unwrap 金额:
1,674,995,512,882,075,429wei - 净 wrap 金额:
-1,572,893,912,977,170,028wei
- Wrap 事件(
- ClickHouse
weth_flows FINAL与daily_weth_net_wrap统计:- Wrap 笔数:4 笔
- Unwrap 笔数:7 笔
wrap_amount:102101599904905401unwrap_amount:1674995512882075429net_wrap_amount:-1572893912977170028- 比对结果:100% 逐字逐位精确匹配(Exact Match)!
3.2 链上样本哈希与事件详情
| 方向 | 区块号 | 日志索引 | 交易哈希 (tx_hash) | 用户地址 (account) | 金额 (wei) |
|---|---|---|---|---|---|
unwrap | 9,705,588 | 71 | 0xd9b7375b9a1b8c3682287f3296ad226397479936fd7c9f428b959a2179d75aaa | 0x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc | 426,741,382,627,020,569 |
wrap | 9,708,691 | 7 | 0x412eeb7350baa20791af9a9bedde52f9c3291af03c2a74efd9d911a8f0718b97 | 0x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc | 10,000,000,000,000,000 |
wrap | 9,709,876 | 3 | 0xf6c41c8ee03913267402b5eb244ee800e38d0fb9d1a995ffa709bcf36249f055 | 0x9fc7a740ec1b338dfbf9a83f5c4109cf2b73f7cc | 71,896,057,984,569,859 |
3.3 重组(Reorg)与墓碑测试
向本地表注入测试日志(区块 9999999):
- 插入版本 1(
is_deleted = 0),查询FINAL结果显示行数由 11 变为 12; - 注入模拟重组墓碑(版本 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 --multiquery4.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;ERC1155 转账与 NFT 持有数据集(erc1155_transfers / erc721_current_owner / erc1155_balances)
对应 SQL:schema/clickhouse/derived/009_erc1155_nft_owners.sql。 依赖已安装的 {db}.logs(sql/schema.sql)和 {db}.erc721_transfers (schema/clickhouse/derived/002_erc721…
Bridge flows(跨链桥流水)
面向直接写 SQL 的内部团队。覆盖 Robinhood Chain(chain id 4663,Arbitrum Orbit) 上 L1<->L2 跨链桥的三类原始信号,派生出 4 张表,全部在 schema/clickhouse/derived/012_bridge_flows.sql。库名以 robi…