跳转到帖子

钱包风控里如何识别“中转归集”地址:基于链上图谱的实用规则

精选回复

发布于

背景

在钱包风控和链上追踪里,很多异常资金不会直接从受害地址流向交易所,而是经过一批中转地址做拆分、合并、跳转,再统一归集。单看某一笔交易很难判断风险,但把地址之间的资金流关系拼起来后,很多模式会变得很明显。

这类地址通常不一定是合约,也不一定有复杂交互,更多是普通 EOA 地址。常见特征包括:短时间内创建或首次活跃、入账来源集中、出账目的集中、余额长期很低、交易间隔规律、资金停留时间短。

技术分析

识别“中转归集”地址,不建议只依赖单一黑名单或单笔金额阈值。更稳妥的方式是结合链上图谱特征和时间特征做评分。

1. 图谱结构特征

  • 入度高、出度低:多个地址向它转入,最终只转给少数地址,常见于归集节点。
  • 入度低、出度低但路径连续:单一来源转入后迅速转出,常见于中转跳板。
  • 资金扇入:多个同类地址在相近时间窗口内向同一地址转账。
  • 资金扇出:一个地址向多个新地址拆分,后续再回流到同一归集点。

2. 时间行为特征

  • 资金停留时间短:入账后几分钟到几小时内转出。
  • 首次活跃时间接近:一批地址在同一时间段首次出现交易。
  • 批量操作节奏:多笔交易 gas price、nonce 增长、时间间隔呈现脚本化特征。

3. 金额特征

  • 转出金额接近入账金额:扣除 gas 后几乎全部转走,余额接近 0。
  • 固定比例拆分:如 0.5、1、2 ETH 或稳定币整数金额频繁出现。
  • 多地址相同金额:同一来源向多个地址发送相近金额,后续流向一致。

操作步骤

实际排查时可以按下面流程做:

  1. 确定起点地址,例如被盗钱包、钓鱼收款地址或已知高危地址。
  2. 抓取一定深度的转账关系,通常先看 2 到 4 跳,不建议一开始无限扩展。
  3. 过滤无关交易,例如小额 dust、明显的空投、无价值代币转账。
  4. 计算每个地址的入度、出度、总入账、总出账、余额变化、资金停留时间。
  5. 对地址打分,优先关注高扇入、快速转出、余额归零的地址。
  6. 把高分地址继续向下游追踪,观察是否进入交易所、跨链桥、混币器或场外归集地址。

代码示例

下面是一个简化示例,用 Python 对已经导出的 ERC-20 转账记录做基础风控评分。这里假设数据来自节点日志、区块浏览器 API 或自建索引服务,字段包括 from、to、value、timestamp、tx_hash。

from collections import defaultdict
from statistics import median

# 示例数据结构
# transfers = [
#     {"from": "0xaaa", "to": "0xbbb", "value": 1000, "timestamp": 1710000000, "tx_hash": "0x..."},
# ]

def build_address_features(transfers):
    incoming = defaultdict(list)
    outgoing = defaultdict(list)

    for t in transfers:
        incoming[t["to"].lower()].append(t)
        outgoing[t["from"].lower()].append(t)

    addresses = set(incoming.keys()) | set(outgoing.keys())
    features = {}

    for addr in addresses:
        ins = incoming.get(addr, [])
        outs = outgoing.get(addr, [])

        in_addrs = set(t["from"].lower() for t in ins)
        out_addrs = set(t["to"].lower() for t in outs)

        total_in = sum(t["value"] for t in ins)
        total_out = sum(t["value"] for t in outs)
        balance_delta = total_in - total_out

        # 计算资金停留时间:取每笔入账后最近一笔出账的时间差
        dwell_times = []
        out_times = sorted(t["timestamp"] for t in outs)
        for i in ins:
            later_outs = [ot for ot in out_times if ot >= i["timestamp"]]
            if later_outs:
                dwell_times.append(later_outs[0] - i["timestamp"])

        median_dwell = median(dwell_times) if dwell_times else None

        features[addr] = {
            "in_degree": len(in_addrs),
            "out_degree": len(out_addrs),
            "tx_in_count": len(ins),
            "tx_out_count": len(outs),
            "total_in": total_in,
            "total_out": total_out,
            "balance_delta": balance_delta,
            "median_dwell_seconds": median_dwell,
        }

    return features


def risk_score(f):
    score = 0

    # 多来源流入,少数目的流出:疑似归集
    if f["in_degree"] >= 5 and f["out_degree"] <= 2:
        score += 30

    # 单跳中转:入账和出账都不多,但很快转走
    if f["in_degree"] <= 2 and f["out_degree"] <= 2:
        if f["median_dwell_seconds"] is not None and f["median_dwell_seconds"] < 3600:
            score += 20

    # 余额接近清空
    if f["total_in"] > 0:
        out_ratio = f["total_out"] / f["total_in"]
        if 0.95 <= out_ratio <= 1.02:
            score += 25

    # 高频收发
    if f["tx_in_count"] + f["tx_out_count"] >= 10:
        score += 15

    # 快速转出
    if f["median_dwell_seconds"] is not None and f["median_dwell_seconds"] < 600:
        score += 10

    return score


features = build_address_features(transfers)
ranked = []

for addr, f in features.items():
    s = risk_score(f)
    if s >= 40:
        ranked.append((addr, s, f))

ranked.sort(key=lambda x: x[1], reverse=True)

for addr, score, f in ranked[:20]:
    print(addr, score, f)

这个脚本不是完整风控系统,但适合作为第一层筛选。真正落地时,建议把 ETH 原生转账、ERC-20 Transfer、内部交易、跨链桥事件分别建表,再统一到地址关系图里。

注意事项

  • 不要只看一条链:很多资金会通过跨链桥转移,单链图谱会断掉。至少要记录桥合约、目标链交易哈希和接收地址。
  • 注意交易所热钱包误报:交易所热钱包天然具备高入度、高出度特征,需要用标签库排除。
  • 区分机器人和攻击者:套利机器人、NFT 批量交易地址也会表现出高频交易,需结合资金来源和下游去向判断。
  • 金额单位要统一:ERC-20 需要根据 decimals 归一化,不同代币不能直接相加。
  • 时间窗口很关键:建议按 10 分钟、1 小时、24 小时分别统计,不同攻击场景节奏差异很大。
  • 保留原始证据:追踪结果最好记录区块高度、交易哈希、日志索引,方便复核和导出报告。

实战判断思路

如果一个地址满足以下组合,优先级可以提高:

  • 上游包含钓鱼、被盗、授权盗转相关地址;
  • 首次收到资金后 10 分钟内转出;
  • 转出金额接近入账金额;
  • 下游继续流向多个新地址,最后进入同一归集地址;
  • 多笔交易的 gas 参数和时间间隔明显接近。

单个特征一般不足以下结论,但多个特征叠加后,基本可以把排查范围缩小很多。

总结

钱包风控和链上追踪的关键不是“看到可疑交易”,而是能把交易放进上下文里判断。中转归集地址的识别,核心在于资金流图谱、时间窗口和余额变化三类特征。

实际工作中,建议先用规则模型做可解释筛选,再结合标签库、交易所地址、跨链桥事件和人工复核。这样既能控制误报,也能在事件发生后快速定位资金路径,为冻结、报案和后续追踪争取时间。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
可以补一个比较实用的判断点:不要只看“多入一出”,中转归集地址更稳定的特征通常是“资金停留时间短 + 输出模式固定 + 与上游地址批次相关”。 实际落规则时可以先做几个可解释的指标:
  • 入账到出账时间差:比如同一地址收到资金后,80% 以上在 5~30 分钟内转出,且长期稳定。
  • 入账来源分散度:来源地址数量多,但每个来源与该地址交互次数少,常见于归集入口。
  • 出账目标集中度:出账大多流向少数几个下游地址,尤其是固定热钱包、兑换入口或下一层中转。
  • 余额残留很低:每次转出后余额接近 dust 或固定保留手续费。
  • 批次同步性:多个上游地址在相近时间窗口向同一地址打款,随后该地址统一转出。
可以按滑动窗口做一个简单评分,不建议单条规则直接封禁。例如:
score = 0

if in_degree_24h >= 20:
    score += 2

if out_degree_24h <= 3 and total_out_value_24h / total_in_value_24h > 0.9:
    score += 3

if median_hold_time < 1800:
    score += 3

if balance_after_transfer_ratio < 0.05:
    score += 2

if source_entropy > 3.0 and target_entropy < 1.0:
    score += 2

if batch_in_ratio > 0.6:
    score += 2

if score >= 8:
    mark_as_possible_aggregation_address()
这里的 source_entropy 和 target_entropy 比单纯统计地址数量更好用,能区分“正常商户收款”和“中转归集”。正常商户可能也有大量入账,但它的出账时间、余额策略和下游结构通常不会这么机械。 如果是 UTXO 链,还可以额外看:
  • 是否频繁出现多 input 合并交易;
  • 找零地址是否规律变化;
  • 归集交易的手续费率是否跟随批处理脚本固定;
  • 同一批 input 的历史来源是否高度相似。
SQL 侧可以先粗筛一批候选:
SELECT
  address,
  COUNT(DISTINCT from_address) AS src_cnt,
  COUNT(DISTINCT to_address) AS dst_cnt,
  SUM(in_value) AS total_in,
  SUM(out_value) AS total_out,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY hold_seconds) AS median_hold
FROM address_flow_1d
WHERE day >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY address
HAVING
  COUNT(DISTINCT from_address) >= 20
  AND COUNT(DISTINCT to_address) <= 5
  AND SUM(out_value) / NULLIF(SUM(in_value), 0) > 0.9
  AND PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY hold_seconds) < 1800;
最后建议把识别结果分成“疑似中转”“高置信归集”“已知服务地址”三类。交易所热钱包、支付网关、矿池分发地址很容易误伤,最好接入标签库做白名单/降权,不然风控策略上线后误报会比较多。

这个评分框架已经比较实用了,我建议再补一个容易误判的点:先区分“地址行为”和“实体行为”。在 UTXO 链上,交易找零地址、批量支付的归集地址经常会表现为多入一出;在账户模型链上,交易所热钱包、桥接合约也可能满足这些条件,但它们的业务周期和交互对象完全不同。

实际落地时,可以把“固定输出目标”细化成目标地址的稳定性,而不是只看数量。例如统计最近 30 天出账目标的 Top1/Top3 占比、目标地址是否发生过标签切换,以及出账是否经过合约调用。普通归集通常是直接转账,桥和 DEX 入口则会出现固定的合约方法、事件日志或 calldata 特征。

def concentration(values, top_n=3):
    total = sum(values)
    if total == 0:
        return 0.0
    return sum(sorted(values, reverse=True)[:top_n]) / total

def score_node(m):
    score = 0

    # 来源多且单一来源交互次数低
    if m.in_degree_24h >= 20 and m.avg_in_count_per_sender <= 2:
        score += 2

    # 快速转出且资金基本不留存
    if m.p80_hold_minutes <= 30 and m.out_in_value_ratio >= 0.90:
        score += 3

    # 出账目标集中,但需要排除已知业务热钱包
    if m.top3_out_value_ratio >= 0.85 and not m.is_known_service_wallet:
        score += 2

    # 多个上游在短窗口内汇入
    if m.max_batch_in_degree_10m >= 8:
        score += 2

    # 同一模式至少持续多个窗口,避免一次性批量活动误报
    if m.active_days_30d >= 7:
        score += 1

    return score

这里的 p80_hold_minutes 最好按单笔资金或可追踪金额计算,不要简单用地址余额时间差,否则会被历史残留余额干扰。对于账户模型,可以按“入账金额桶”做近似匹配;对于 UTXO,则优先按输入输出金额和找零启发式匹配,并把手续费单独扣除。

另外建议增加一个“模式稳定性”维度:同一地址连续几个滑动窗口都达到阈值才进入人工复核,例如 6 个窗口中至少 4 个命中。最终动作也不要直接封禁,可以先标记为疑似中转、提高交易审核级别,并保留上游批次、出账目标和时间差作为解释字段。这样既能降低交易所批处理、商户收款和桥接业务带来的误报,也方便后续复盘规则效果。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。