BlockVectra

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 ApprovalERC721 Approval碰撞过滤规则
topic00x8c5be1...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 --multiquery

4.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,911
  • erc20_approvals FINAL WHERE is_deleted = 0 的行数:9,911
  • 精确匹配:9,911 = 9,911(无遗漏、无虚增)。

5.2 ERC721 隔离性验证

  • 同一区块区间内,topic0 = Approval 的日志总数为 10,139 条。
  • 其中 topic3 IS NOT NULL(即 ERC721 Approval(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_approvals Value: 57746267935224508256684860(✅ 完全一致)
  • Log Index 48(同区块后续重置授权为 0):
    • Token/Owner/Spender 与上述一致
    • Python 解码 Value: 0
    • ClickHouse erc20_approvals Value: 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)验证

  1. 向 logs 表模拟插入针对区块 72057618、log_index = 59 的墓碑行(version = 2000000000000000000,is_deleted = 1)。
  2. 物化视图 mv_erc20_approvals 瞬间自动触发,写入对应主键的墓碑行。
  3. 执行 SELECT count() FROM erc20_approvals FINAL WHERE block_number = 72057618 AND log_index = 59 AND is_deleted = 0,返回 0(成功消除)。
  4. 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 统计:
    1. 0x0000000000001ff3684f28c67538d4d072c22734:19,351 次授权,涵盖 1,327 种代币与 1,120 个独立用户(Uniswap Permit2 官方部署合约)。
    2. 0x6131b5fae19ea4f9d964eac0408e4408b66337b5:18,722 次授权,涵盖 682 种代币与 325 个独立用户(主流 DEX 路由合约)。
    3. 0xccc88a9d1b4ed6b0eaba998850414b24f1c315be:13,509 次授权,涵盖 1,148 种代币与 6,769 个独立用户。
    4. 0x4cd00e387622c35bddb9b4c962c136462338bc31:13,336 次授权,涵盖 2 种代币与 403 个独立用户。
    5. 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;

本页目录