BlockVectra

Token 供应量数据集(Token Supply)

来源:schema/clickhouse/derived/019_token_supply.sql。 面向内部用 SQL 追踪和分析各 ERC-20 代币每日发行量(Mint)、销毁量(Burn)、每日净增量(Net Mint)以及历史累计流通供应量(Cumulative Supply)的团队。

来源:schema/clickhouse/derived/019_token_supply.sql。 面向内部用 SQL 追踪和分析各 ERC-20 代币每日发行量(Mint)、销毁量(Burn)、每日净增量(Net Mint)以及历史累计流通供应量(Cumulative Supply)的团队。


1. 概述与核心功能

以太坊与 EVM 兼容链上,代币的总供应量(totalSupply)是合约状态变量,标准 ERC-20 没有独立的"每日供应量快照"事件。 本项目通过解析 {db}.erc20_transfers 中的零地址转账:

  • Mint(铸造 / 发行):from = 0x0000000000000000000000000000000000000000
  • Burn(销毁):to = 0x0000000000000000000000000000000000000000 AND from != 0x0000000000000000000000000000000000000000

在每个已收盘的 UTC 日历日(toDate(block_timestamp) < today())执行幂等刷新,计算当日发行额、销毁额、净发行额,并使用时间窗口函数累加自创世以来的全部净发行额,得到每日收盘时的历史累计代币供应量(cumulative_supply)。同时提供实时全量视图与链上 spot-check 脚本,用于对齐节点 eth_call totalSupply()。


2. 架构设计与正确性规则

2.1 为什么采用收盘日幂等刷新(Option (b)),而不是插入触发物化视图

根据项目架构规范与正确性准则:

  1. 原始表 {db}.erc20_transfers 是 ReplacingMergeTree(version, is_deleted)。在断点续跑(backfill resume)时会重叠插入重复数据(相同排序键与版本),在发生链重组(reorg)时会写入墓碑行(is_deleted = 1,更高版本)。
  2. 普通的插入触发物化视图(Insert-trigger MV)对每批物理 INSERT 执行增量 sum()/count()。该机制在后台去重 merge 之前触发,无法识别重复插入与重组回退,会导致严重双重计数(double-counting)。
  3. 因此,本数据集严格遵循 CORRECTNESS RULE 方案 (b):
    • 目标表 {db}.token_daily_supply 由可刷新物化视图(REFRESH EVERY 1 DAY OFFSET 1 HOUR RANDOMIZE FOR 10 MINUTE)驱动。
    • 仅对已过去的完整收盘日(toDate(block_timestamp) < today())进行聚合。
    • 读取数据源时强制使用 FINAL 并过滤 is_deleted = 0,保证重复版本与重组墓碑均已折叠清除。
    • 在 ClickHouse 26.x 中,未带 APPEND 的可刷新物化视图在每次调度触发时对目标表执行整表原子替换(Atomic Swap),天然完全幂等,杜绝一切历史漂移。

3. 金额精度与类型规范(Money-Safety)

  • 绝对禁止浮点数:严禁在代币金额、发行量、销毁量计算中使用 Float32 / Float64。
  • 原始数量保持 UInt256:mint_amount 和 burn_amount 严格使用 UInt256 存储链上原始数量(Wei),无任何提前除以 $10^{decimals}$ 的缩放,保证精度零损失。
  • 净变化量与累计量使用有符号 Int256: $$\text{net_mint_amount} = \text{toInt256}(\text{mint_amount}) - \text{toInt256}(\text{burn_amount})$$ 代币在某一天销毁量大于铸造量是常见且合法的经济行为(如大额赎回销毁),每日净增量可能为负数,必须使用有符号整数。合法代币总量远在 $2^{255}$ 范围以内,toInt256 转换精确无溢出风险。
  • Fail-closed 异常大额过滤(amount >= 2^255): 遵循 027_erc20_balances.sql 既有惯例,显式增加 AND amount < bitShiftLeft(toUInt256(1), 255)。恶意 Honeypot 或垃圾代币铸造天文数字(如 type(uint256).max)会导致 sumIf 发生 $2^{256}$ 回绕并在转为 Int256 时变为大负数,污染发布数据。此类异常值直接在入库前 fail-close 丢弃(dropped, not recorded wrong)。
  • 时间窗口累计(Window Sum)与创世回填说明:
    sum(toInt256(mint_amount) - toInt256(burn_amount)) OVER (
        PARTITION BY token
        ORDER BY day ASC
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_supply
    精确累加该代币从链历史上第一次铸造直到当日收盘的总量。注意:若 {db}.erc20_transfers 尚未回填至创世区块(genesis),历史铸造未入库而在窗口期内发生销毁,可能使未完全回填的代币暂时呈现负数 cumulative_supply(与 027_erc20_balances.sql 相同,属于源数据回填范围特性,源表完整回填后自然准确)。

4. 链上真实语义与边界情况(Edge Cases)

在 Robinhood Chain(chain id 4663)真实链上数据与节点 RPC 验证中,观测并处理了以下典型场景:

4.1 Uniswap V2 配对初始化永久锁定流动性(Transfer(0, 0, 1000))

  • 现象:Uniswap V2 Pair 初始化首次添加流动性时,会调用 _mint(address(0), MINIMUM_LIQUIDITY) 永久锁死 1,000 Wei 的 LP 代币。该操作触发的事件为 from = 0x00...00 且 to = 0x00...00,金额为 1000。
  • 合约行为:Pair 合约内部执行 totalSupply = totalSupply + 1000,并记录在 balanceOf[address(0)] = 1000,合约的 totalSupply() 确实包含了这 1,000 Wei。
  • 本方案处理: 若仅简单判断 to = 0 计为销毁,则该笔交易同时记为 +1000 和 -1000,相互抵消导致累计量少算 1,000 Wei。 因此销毁条件显式限制为 to = 0x00...00 AND from != 0x00...00,使得 Transfer(0, 0, 1000) 正确被计为铸造入库,累计值与链上 totalSupply() 精确至个位数(Wei)完全对齐。

4.2 构造函数未发事件的初始铸造(Constructor Unminted Supply)

  • 现象:少数早期或非标准代币合约在 constructor 中直接给特定地址赋值状态变量 balanceOf[deployer] = INITIAL_SUPPLY; totalSupply = INITIAL_SUPPLY;,未触发 emit Transfer(address(0), ...)。
  • 表现:此类代币从 Transfer 日志计算的 cumulative_supply 会小于节点 eth_call totalSupply()。Spot-check 脚本对此类代币会自动打标为 DIFF_UNMINTED。

4.3 销毁至黑洞地址(Dead Address Burn)

  • 现象:部分项目通过向 0x000000000000000000000000000000000000dead 等非零地址转账实现"社区销毁"。
  • 表现:因其未调用标准 _burn(即未转入零地址),合约状态变量 totalSupply() 并未减少。本数据集遵循 ERC-20 规范,仅将转至零地址(address(0))视为正式销毁。

4.4 普通转账至零地址 vs 真正 _burn

  • 现象:部分简易合约的 transfer 函数未对接收方为 address(0) 做特殊拦截与 totalSupply 扣减。用户转入零地址只是转移了持有者,totalSupply() 并没有变化。
  • 表现:Spot-check 脚本会自动对比并标明 DIFF_BURN_TRANSFER(transfer to 0x0 did not reduce totalSupply on-chain)。

5. 数据表与视图结构

5.1 {db}.token_daily_supply(按天历史供应量表)

CREATE TABLE IF NOT EXISTS {db}.token_daily_supply
(
    token              FixedString(20),
    day                Date,
    mint_count         UInt64,
    burn_count         UInt64,
    mint_amount        UInt256,
    burn_amount        UInt256,
    net_mint_amount    Int256,
    cumulative_supply  Int256,
    refreshed_at       DateTime('UTC')
)
ENGINE = ReplacingMergeTree(refreshed_at)
PARTITION BY toYYYYMM(day)
ORDER BY (token, day);
字段类型说明
tokenFixedString(20)代币合约地址(二进制,可用 hex() 输出)
dayDate已收盘的 UTC 日历日
mint_countUInt64当日铸造事件笔数(from = 0x00...00)
burn_countUInt64当日销毁事件笔数(to = 0x00...00 AND from != 0x00...00)
mint_amountUInt256当日总铸造数量(未缩放整数,单位为代币原始最小单位 Wei)
burn_amountUInt256当日总销毁数量(未缩放整数,单位为代币原始最小单位 Wei)
net_mint_amountInt256当日净铸造数量(mint_amount - burn_amount,可为负)
cumulative_supplyInt256截至当日收盘的历史累计代币供应量(时间窗口求和)
refreshed_atDateTime('UTC')视图或回填任务最近刷新时间

5.2 {db}.v_token_latest_supply(收盘日最新供应量摘要视图)

直接读取 token_daily_supply FINAL,提供每个代币在最近一个收盘日的全局聚合快照(总发行、总销毁、最新累计供应量)。查询开销极小。

5.3 {db}.v_token_live_supply(全链实时供应量视图)

直接读取 {db}.erc20_transfers FINAL,包含今日尚未收盘的最新实时区块数据,供随时随地对齐节点最新高度的 eth_call totalSupply()。


6. Spot-Check 抽检脚本(spot_check_token_supply.py)

仓库提供专用自动化验证脚本:scripts/derived/spot_check_token_supply.py。

运行方式

# 环境变量指定连接信息
export CH_URL="http://127.0.0.1:8123"
export CH_USER="indexer"
export CH_PASSWORD="indexer"
export CH_DB="robinhood"
export RPC_URL="https://5.9.43.222/<KEY>"

# 1. 批量抽检活跃代币
python3 scripts/derived/spot_check_token_supply.py --ch-db robinhood --limit 20

# 2. 检查指定代币合约
python3 scripts/derived/spot_check_token_supply.py --ch-db robinhood --token 0x5ebc7d0f2d01ed9ffe3f0617d30cb09664a17777

# 3. 切换检查模式(live: 包含今日实时转账;closed: 仅检查已收盘日)
python3 scripts/derived/spot_check_token_supply.py --ch-db robinhood --mode closed --limit 20

7. 本地验证(Verification & Evidence)

在本地独立 ClickHouse 实例上建立独立临时库 wt44_token_supply,使用预编译 release 二进制回填区块 [39250642, 39255642](2026-08-18 已收盘日,5,000 个区块,共 60,222 笔交易、179,209 条日志、72,152 笔 ERC-20 转账)。

7.1 验证用例 1:代币在其生命周期内完全包含在抽样区间内且无后续转账

在此区间内创建且无后续转账的代币,ClickHouse 中计算的 cumulative_supply 与节点真实 eth_call totalSupply() 达成 100% 精确匹配(精确到个位数 Wei):

代币地址 (token)首次出现区块ClickHouse cumulative_supply节点 RPC totalSupply()结果
0x5ebc7d0f2d01ed9ffe3f0617d30cb09664a177773925243010000000000000000000000000001000000000000000000000000000EXACT MATCH
0x1829cf9dca23913693eddc58423f489fe005cccc3925346410000000000000000000000000001000000000000000000000000000EXACT MATCH
0x4d22495cdbc7ed0cb4b692ca942dbe9e482577773925154710000000000000000000000000001000000000000000000000000000EXACT MATCH
0x2f7ee2d4265b0d9c808b506c10c2f7aeb77177773925083110000000000000000000000000001000000000000000000000000000EXACT MATCH
0x55f1d365370fa1d3e7a0bb7f68016d22f0bdcccc3925101510000000000000000000000000001000000000000000000000000000EXACT MATCH
0x941959ee6048d1743f2382b1bb1b80d0007277773925502210000000000000000000000000001000000000000000000000000000EXACT MATCH
0xd2d60ade6a0459f4c0abb0bf57e27bd50b7c77773925342210000000000000000000000000001000000000000000000000000000EXACT MATCH
0xd66d81e5af56c5c6870d4252566169adce52cccc3925156710000000000000000000000000001000000000000000000000000000EXACT MATCH
0xf2f1dcbcd3d36030415e4a7922b3b6ebe7d977773925504110000000000000000000000000001000000000000000000000000000EXACT MATCH
0xfc8da382f4816e032427024e76b1c7e9c61077773925504610000000000000000000000000001000000000000000000000000000EXACT MATCH
0x1e1a7cbde7fdefbcbc7c549d509ad1cf35d929093925235000EXACT MATCH

7.2 验证用例 2:Uniswap V2 Pair 复杂铸造与销毁

在生产环境区块 72045539 至 72045627 追踪代币 0xa5ef7104629a38633fc453308fd97f9068ab6e25:

  • 事件 1(块 72045539,log 25):from = 0, to = 0, amount = 1000(初始化锁死)
  • 事件 2(块 72045539,log 26):from = 0, to = user, amount = 999999999999999999000
  • 事件 3(块 72045627,log 1):from = 0, to = user2, amount = 13123678961433
  • 事件 4(块 72045627,log 2):from = contract, to = 0, amount = 999999999999999999000(赎回销毁)

计算: $$\text{总发行} = 1000 + 999999999999999999000 + 13123678961433 = 1000000013123678961433$$ $$\text{总销毁} = 999999999999999999000$$ $$\text{净累计量} = 1000000013123678961433 - 999999999999999999000 = 13123678962433$$

调用节点 RPC: eth_call(0xa5ef7104629a38633fc453308fd97f9068ab6e25, "0x18160ddd") 返回: 0x00000000000000000000000000000000000000000000000000000bef98390301 = 13123678962433。 两边数值完全一致,精确到个位。


8. 安装与补算

8.1 安装步骤

  1. 确认前置表 {db}.erc20_transfers 已建好并已回填(参见 001_erc20_transfers.sql)。
  2. 执行 SQL 文件(替换 {db} 为真实库名,如 robinhood):
    sed 's/{db}/robinhood/g' schema/clickhouse/derived/019_token_supply.sql | clickhouse-client --multiquery
    若通过 HTTP 接口执行,请将文件内的各条 SQL 语句以分号切分后逐条 POST。
  3. 文件内已包含一次性全量历史回填 INSERT INTO {db}.token_daily_supply ...,执行完即已包含当前所有历史已收盘日的代币供应量统计。

8.2 定时刷新与运维监控

  • 视图 mv_token_daily_supply 声明了:
    REFRESH EVERY 1 DAY OFFSET 1 HOUR RANDOMIZE FOR 10 MINUTE
    在每天 UTC 01:00(±10分钟内随机抖动)自动触发刷新,以整表原子替换方式更新全部已收盘日数据。
  • 手动立即触发刷新:
    SYSTEM REFRESH VIEW robinhood.mv_token_daily_supply;
    SYSTEM WAIT VIEW robinhood.mv_token_daily_supply;
  • 查看刷新状态与历史日志:
    SELECT database, view, status, last_refresh_time, last_success_time, next_refresh_time, exception
    FROM system.view_refreshes
    WHERE database = 'robinhood' AND view = 'mv_token_daily_supply';

9. 常用分析查询示例

9.1 查询某代币的历史每日供应量演变

SELECT
    day,
    mint_count,
    burn_count,
    mint_amount,
    burn_amount,
    net_mint_amount,
    cumulative_supply
FROM robinhood.token_daily_supply FINAL
WHERE token = unhex('5ebc7d0f2d01ed9ffe3f0617d30cb09664a17777')
ORDER BY day ASC;

9.2 查询某天发行量(Mint)最大的前 10 个代币

SELECT
    hex(token) AS token_address,
    mint_count,
    mint_amount,
    burn_amount,
    cumulative_supply
FROM robinhood.token_daily_supply FINAL
WHERE day = '2026-08-18'
ORDER BY mint_amount DESC
LIMIT 10;

9.3 结合 tokens 元数据表换算人类可读供应量(若已安装 tokens 表)

SELECT
    hex(s.token) AS token_address,
    t.symbol,
    s.latest_day,
    s.cumulative_supply,
    multiIf(
        t.decimals = 18, toDecimal256(s.cumulative_supply, 18),
        t.decimals = 6,  toDecimal256(s.cumulative_supply, 6),
        toDecimal256(s.cumulative_supply, 0)
    ) AS supply_formatted
FROM robinhood.v_token_latest_supply AS s
LEFT JOIN robinhood.tokens AS t FINAL ON t.address = s.token
WHERE s.cumulative_supply > 0
ORDER BY s.total_mint_count DESC
LIMIT 10;

本页目录