ERC20 授权数据集(erc20_approvals / erc20_current_allowances)
对应 SQL 文件:schema/clickhouse/derived/018_erc20_approvals.sql。 依赖已安装的 {db}.logs(sql/schema.sql,由底层的 raw loader 写入并维护)。
对应 SQL 文件:schema/clickhouse/derived/018_erc20_approvals.sql。
依赖已安装的 {db}.logs(sql/schema.sql,由底层的 raw loader 写入并维护)。
1. 概念与事件签名
1.1 事件签名与哈希推导
ERC20 Approval 事件标准定义(EIP-20):
event Approval(address indexed owner, address indexed spender, uint256 value);规范签名文本为 Approval(address,address,uint256)。
其 Keccak-256 哈希计算如下(独立验证,非记忆):
from Crypto.Hash import keccak
k = keccak.new(digest_bits=256)
k.update(b'Approval(address,address,uint256)')
print(k.hexdigest())
# => 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925对应十六进制常量:
topic0 = unhex('8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925')。
1.2 严格区分 ERC20 与 ERC721 Approval
ERC721 的 Approval 事件签名同样是 Approval(address,address,uint256):
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);由于 Solidity 事件 topic0 仅由函数名和参数类型列表哈希决定,参数是否 indexed 不影响 topic0,因此两者发生 topic0 碰撞(与 Transfer 事件在 001/002 中的碰撞机制完全相同):
| 特性 | ERC20 Approval | ERC721 Approval | 碰撞过滤规则 |
|---|---|---|---|
topic0 | 0x8c5be1... | 0x8c5be1... | 相同 |
topic1 (owner) | address (20 bytes zero-padded) | address (20 bytes zero-padded) | topic1 IS NOT NULL |
topic2 (spender / approved) | address (20 bytes zero-padded) | address (20 bytes zero-padded) | topic2 IS NOT NULL |
topic3 (tokenId) | NULL (无第三个 indexed 参数) | NOT NULL (uint256 tokenId) | topic3 IS NULL |
data 长度 | 32 bytes (uint256 value) | 0 bytes (空数据) | length(data) = 32 |
在物化视图与回填过滤条件中,通过 topic3 IS NULL AND length(data) = 32 实现 Fail-Closed 严格区分,确保 ERC721 授权日志与异常日志 100% 隔离,绝不污染本表。
2. 核心语义与设计权衡
2.1 语义说明:"最后批准额度"(Last Approved Value),非实时链上余额
[!WARNING] 本表及相关视图反映的是链上显式触发的
approve(...)所产生的Approval事件记录。 在绝大多数 ERC20 标准实现中,transferFrom(owner, recipient, amount)在划转代币时会静默扣减 spender 的 allowance,而不发出任何事件。因此,如果某次授权之后发生过
transferFrom,真实的链上可用额度会小于本表记录的授权值。业务方若需要实时的、扣减后的链上额度,仍需调用代币合约的allowance(owner, spender)视图接口。
2.2 金额安全性(Money-Safety)
value严格保留为原始链上UInt256整数,使用大端字节反转:reinterpretAsUInt256(reverse(data))。- 绝不使用浮点数(Float32/Float64),禁止任何有损转换。
2.3 正确性规则:方案 (a)
遵循仓库《派生表正确性规则》:
- 原始 ReplacingMergeTree 表在 backfill 断点续传或 reorg 墓碑(
is_deleted=1,更高version)下会产生重复插入。 - 本数据集采用 方案 (a):
{db}.erc20_approvals采用ENGINE = ReplacingMergeTree(version, is_deleted),主键包含区块与日志序号ORDER BY (token, owner, spender, block_number, log_index),与源日志 1:1 严格对齐。- 物化视图
mv_erc20_approvals直接透传源表{db}.logs的version与is_deleted。 - 当发生区块链回滚时,底层写出的墓碑行会被 MV 同步转换为
is_deleted = 1的同键记录,在FINAL WHERE is_deleted = 0过滤下自动消除,与源表回滚完全同步。
- 为什么不选方案 (b)(定时周期整表刷新):
- 授权记录本身是细粒度日志流,不需要预先聚合求和;
- "当前授权额度"和"高频被授权方"由普通视图(普通 View 每次查询直接下推到
erc20_approvals FINAL WHERE is_deleted = 0)动态聚合计算,永远保持最新且天然幂等,无需维护定时刷新任务。
3. 表结构与视图定义
3.1 明细表:{db}.erc20_approvals
CREATE TABLE IF NOT EXISTS {db}.erc20_approvals
(
token FixedString(20),
owner FixedString(20) CODEC(ZSTD(3)),
spender FixedString(20) CODEC(ZSTD(3)),
value UInt256 CODEC(ZSTD(3)),
block_number UInt64 CODEC(Delta, ZSTD),
block_timestamp DateTime('UTC') CODEC(Delta, ZSTD),
tx_hash FixedString(32) CODEC(ZSTD(3)),
tx_index UInt32 CODEC(Delta, ZSTD(3)),
log_index UInt32 CODEC(Delta, ZSTD(3)),
version UInt64,
is_deleted UInt8,
INDEX idx_owner owner TYPE bloom_filter GRANULARITY 4,
INDEX idx_spender spender TYPE bloom_filter GRANULARITY 4
)
ENGINE = ReplacingMergeTree(version, is_deleted)
PARTITION BY intDiv(block_number, 5000000)
ORDER BY (token, owner, spender, block_number, log_index);3.2 视图 1:最新授权额度 {db}.erc20_current_allowances
按 (token, owner, spender) 三元组分组,使用 argMax(value, (block_number, log_index)) 获取该授权关系的最新一次设定值:
CREATE VIEW IF NOT EXISTS {db}.erc20_current_allowances AS
SELECT
token,
owner,
spender,
argMax(value, (block_number, log_index)) AS value,
max(block_number) AS last_block_number,
argMax(tx_hash, (block_number, log_index)) AS last_tx_hash
FROM {db}.erc20_approvals FINAL
WHERE is_deleted = 0
GROUP BY token, owner, spender;3.3 视图 2:无限授权视图 {db}.erc20_unlimited_approvals
筛选出最新授权值为 2^256 - 1(无符号 256 位全 1)的记录。此为 DeFi 应用(如 Uniswap、Permit2 等)常见的“一次授权无限使用”模式:
CREATE VIEW IF NOT EXISTS {db}.erc20_unlimited_approvals AS
SELECT token, owner, spender, value, last_block_number, last_tx_hash
FROM {db}.erc20_current_allowances
WHERE value = bitNot(toUInt256(0));3.4 视图 3:高频 Spender 统计 {db}.erc20_top_spenders_by_approval_count
统计历史所有非删除授权中,获得授权次数最多的 spender 地址,以及对应的独立代币数和持有人数:
CREATE VIEW IF NOT EXISTS {db}.erc20_top_spenders_by_approval_count AS
SELECT
spender,
count() AS approval_count,
uniqExact(token) AS distinct_tokens,
uniqExact(owner) AS distinct_owners
FROM {db}.erc20_approvals FINAL
WHERE is_deleted = 0
GROUP BY spender
ORDER BY approval_count DESC;4. 安装与补算
4.1 安装命令
在链数据库已包含 {db}.logs 前提下,执行以下命令安装表、物化视图、视图并完成存量数据回填:
sed 's/{db}/robinhood/g' schema/clickhouse/derived/018_erc20_approvals.sql \
| clickhouse-client --multiquery4.2 补算(Backfill)说明与幂等性
- 物化视图
mv_erc20_approvals仅处理创建之后写入的日志。 - 文件内自带存量
INSERT INTO {db}.erc20_approvals SELECT ... FROM {db}.logs语句。 - 幂等性保障:目标表以
(token, owner, spender, block_number, log_index)为排序主键,且引擎为ReplacingMergeTree(version, is_deleted)。重复执行回填或多次运行安装脚本,重复行的主键和版本一致,在 ClickHouse 后台合并与FINAL查询中完全去重,行数精确不变。
5. 验证结果与研究证据
本数据集在本地独立 scratch 数据库(5,000 区块,区块区间 [72054348, 72059347])与生产环境(只读研究)进行了完整验证。
5.1 本地 ClickHouse 精确匹配(Exact Match)
在测试区间 5,000 个区块、148,118 条原始 logs 中:
logs FINAL中符合 ERC20 Approval 条件(topic0 匹配,topic1/2 非空,topic3 为空,data 为 32 字节)的行数:9,911erc20_approvals FINAL WHERE is_deleted = 0的行数:9,911- 精确匹配:9,911 = 9,911(无遗漏、无虚增)。
5.2 ERC721 隔离性验证
- 同一区块区间内,
topic0 = Approval的日志总数为 10,139 条。 - 其中
topic3 IS NOT NULL(即 ERC721Approval(owner, approved, tokenId))共有 228 条,其length(data)均为 0。 10,139 = 9,911 (ERC20) + 228 (ERC721),异常日志数:0。erc20_approvals中包含的 ERC721 日志数:0(验证了topic3 IS NULL的排他性)。
5.3 链上原始 RPC 日志独立交叉验证
编写独立 Python 脚本调用 Robinhood Chain 节点 RPC eth_getBlockReceipts,抓取原生 JSON 并做独立大端整数解码,逐字段比对:
案例 1:普通授权与撤销(同一区块内多次状态更迭)
- 区块:
72056432(十六进制0x44b7e70),交易哈希:0x463dd0daa8ea8cb14a96e45cf345c6a87a0bc1bc970738e2e9cfe1534d80653d - Log Index 24(初次授权):
- Token:
0x00feb898c5de618ebf991ac99db6534156161ba3 - Owner:
0xb300000b72deaeb607a12d5f54773d1c19c7028d - Spender:
0x30446f3fe295f9137272502dcea6fdf1e5d8eb13 - RPC 原生 Data:
0x000000000000000000000000000000000000000000000002fc43f7519269f646 - Python 解码 Value:
57746267935224508256684860 - ClickHouse
erc20_approvalsValue:57746267935224508256684860(✅ 完全一致)
- Token:
- Log Index 48(同区块后续重置授权为 0):
- Token/Owner/Spender 与上述一致
- Python 解码 Value:
0 - ClickHouse
erc20_approvalsValue:0(✅ 完全一致)
- 当前额度视图验证:
erc20_current_allowances查询该三元组,argMax(value, (block_number, log_index))精确输出0,成功捕获区块内更新。
案例 2:无限授权(2^256 - 1)
- 区块:
72056432,Log Index:0,交易哈希:0xc1cf82282a23afcd9f9f589f05d9fa3168db07d859e80ce06fcbb9f54c758a38 - Token:
0x00feb898c5de618ebf991ac99db6534156161ba3 - Owner:
0xff9c872f48abf85d3427efc74fed767f3b753d5e - Spender:
0xb300000b72deaeb607a12d5f54773d1c19c7028d - RPC 原生 Data:
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - Python 解码 Value:
115792089237316195423570985008687907853269984665640564039457584007913129639935 - ClickHouse
erc20_unlimited_approvals视图成功命中该记录,Value 精确匹配bitNot(toUInt256(0))(✅ 完全一致)。
5.4 重组回滚(Reorg Tombstone)验证
- 向
logs表模拟插入针对区块72057618、log_index = 59的墓碑行(version = 2000000000000000000,is_deleted = 1)。 - 物化视图
mv_erc20_approvals瞬间自动触发,写入对应主键的墓碑行。 - 执行
SELECT count() FROM erc20_approvals FINAL WHERE block_number = 72057618 AND log_index = 59 AND is_deleted = 0,返回 0(成功消除)。 erc20_current_allowances视图自动回退至上一有效版本,验证了方案 (a) 对重组的完备支持。
5.5 生产环境链上数据调研证据
在只读生产库中针对近期 block_number >= 72050000(约 10 万个区块)进行统计:
- ERC20 Approval 事件总数:214,759 行。
- ERC721 Approval 事件总数:4,484 行。
- 异常长度(
length(data) != 32 AND topic3 IS NULL)事件数:0 行。 - Top 5 Spender 统计:
0x0000000000001ff3684f28c67538d4d072c22734:19,351 次授权,涵盖 1,327 种代币与 1,120 个独立用户(Uniswap Permit2 官方部署合约)。0x6131b5fae19ea4f9d964eac0408e4408b66337b5:18,722 次授权,涵盖 682 种代币与 325 个独立用户(主流 DEX 路由合约)。0xccc88a9d1b4ed6b0eaba998850414b24f1c315be:13,509 次授权,涵盖 1,148 种代币与 6,769 个独立用户。0x4cd00e387622c35bddb9b4c962c136462338bc31:13,336 次授权,涵盖 2 种代币与 403 个独立用户。0x039ec98a76f111092d4751365ff09dd2aec301e8:13,005 次授权,涵盖 559 种代币与 1 个独立用户(特定做市/套利机器人)。
6. 常见查询示例
示例 1:查询某用户对特定代币的所有授权现状
SELECT
hex(token) AS token_address,
hex(spender) AS spender_address,
value AS last_approved_value,
last_block_number,
hex(last_tx_hash) AS last_tx_hash
FROM robinhood.erc20_current_allowances
WHERE owner = unhex('ff9c872f48abf85d3427efc74fed767f3b753d5e')
AND token = unhex('00feb898c5de618ebf991ac99db6534156161ba3');示例 2:查询某用户持有的所有无限授权(风险敞口排查)
SELECT
hex(token) AS token_address,
hex(spender) AS spender_address,
last_block_number,
hex(last_tx_hash) AS tx_hash
FROM robinhood.erc20_unlimited_approvals
WHERE owner = unhex('ff9c872f48abf85d3427efc74fed767f3b753d5e');示例 3:查询某个 Spender 获批授权最多的代币排行
SELECT
hex(token) AS token_address,
count() AS approval_count,
uniqExact(owner) AS distinct_owners
FROM robinhood.erc20_approvals FINAL
WHERE spender = unhex('0000000000001ff3684f28c67538d4d072c22734')
AND is_deleted = 0
GROUP BY token
ORDER BY approval_count DESC
LIMIT 10;示例 4:查询某笔交易中发生的所有 ERC20 授权明细
SELECT
log_index,
hex(token) AS token,
hex(owner) AS owner,
hex(spender) AS spender,
value
FROM robinhood.erc20_approvals FINAL
WHERE tx_hash = unhex('c1cf82282a23afcd9f9f589f05d9fa3168db07d859e80ce06fcbb9f54c758a38')
AND is_deleted = 0
ORDER BY log_index;示例 5:统计某代币的无限授权用户占比
SELECT
count() AS total_allowances,
countIf(value = bitNot(toUInt256(0))) AS unlimited_allowances,
round(unlimited_allowances * 100.0 / total_allowances, 2) AS unlimited_percentage
FROM robinhood.erc20_current_allowances
WHERE token = unhex('00feb898c5de618ebf991ac99db6534156161ba3')
AND value > 0;