
简介基于源代码的图融合的智能合约漏洞检测项目是一份面向区块链、计算机及相关专业学生的高分本科毕业设计成果。项目答辩评审98分代码均经过调试可运行覆盖原始代码预处理、数据流与控制流信息融合、文档分类、漏洞标签构造如重入、算术、时间戳攻击及模型训练与预测等完整流程。包体共48个文件以35个Python脚本为核心配合XML工程配置、TXT环境说明、YML依赖描述、DOCX手册及示例数据集ZIP整体压缩包仅14.84MB目录结构清晰便于按模块复现实验。资源特别适合毕业设计、课程大作业或入门智能合约安全的进阶学习已有111人学习下载。完整源码与手册能帮助读者理解图融合检测思路可在此基础上修改扩展实现更多漏洞类型检测。1. 图融合不是把图拼起来智能合约漏洞检测的毕业设计核心在哪只把 Solidity 源码解析成 AST 再丢给 GNN漏洞检测准确率通常卡在 80% 上下重入漏洞跨函数识别率更低。原因不是模型能力不够而是单一源代码表示承载不了完整程序语义AST 没有控制流顺序CFG 缺少表达式级数据依赖DFG 又难以处理函数调用边界。基于源代码的图融合就是把 AST、CFG、DFG 这些程序分析产物按节点对齐成一张异质图再训练图神经网络做分类或节点定位这也是当前智能合约静态分析和大量高质量论文在用的主流方案。这篇文章按“任务定义 → 多图构建 → 融合建模 → 训练验证 → 可解释落地”的顺序把可复现的套路讲清楚既适合做区块链方向毕业设计的人直接套用也适合想评估图神经网络在代码分析上真实边界的从业者。2. 智能合约漏洞检测的目标与图表示选型2.1 检测什么SWC 漏洞类别与脆弱模式智能合约漏洞检测本质上是一个程序分析任务输入是合约源代码输出是有没有漏洞、漏洞在哪一类、触发点在哪个调用链上。毕业设计和工业扫描器最大的区别在于论文要有明确定义的标签体系不能只输出“有风险”这种模糊结果。我一般直接用 SWC 注册表的分类作为标签每类漏洞都有明确的触发模式和需要依赖的程序信息。漏洞类型SWC 编号典型触发模式检测所需程序信息重入漏洞SWC-107外部调用后再修改状态跨函数调用图 状态变量写操作的先后顺序整数溢出SWC-101算术运算结果溢出表达式 AST 算术运算符传播路径未检查的 call 返回值SWC-104外部调用返回值未验证调用表达式 控制流分支结构tx.origin 认证SWC-115用 tx.origin 做权限校验表达式 AST 函数调用链时间戳依赖SWC-116用 block.timestamp 做随机数表达式 AST 比较操作上下文拒绝服务SWC-113循环外部调用或 require 陷阱循环结构 跨函数状态读写这个表格直接决定了后面建图时哪些边必须保留。比如重入检测需要“外部调用边”和“状态变量写边”同时存在如果建图时只保留了 AST 父子边模型无论如何都学不出调用顺序关系。2.2 为什么单一源代码表示会漏报AST 能表达语法结构但表达不了程序执行的先后顺序。a 1; a a 1;和a a 1; a 1;的 AST 结构高度相似都是赋值表达式套加法表达式但语义完全不同——前者最终值是 2后者是 1。GNN 在这种结构相似、语义相反的样本上容易泛化失败。CFG 补上了执行顺序但 CFG 的基本块内部表达式之间的数据关系是丢失的。整数溢出检测需要知道某个变量在进入算术运算之前是否被外部输入污染这条信息在 CFG 的跳转边上根本不存在要靠 def-use 链往上追溯。DFG 补上了变量定义到使用的传播路径但纯 DFG 面对函数调用边界时会断开。Solidity 里最常见的重入模式恰恰是msg.sender.call.value()()发生在函数 A状态变量更新在函数 BA 调用 B 之后才发生状态更新。函数间调用信息属于调用图CG的范畴单靠函数内 DFG 覆盖不到。多传感器融合建图定位领域有个类似直觉GPS、IMU、视觉各自有盲区先做时间戳对齐再做状态融合比任何单一传感器都稳。源代码的图融合也是同一套思想——先对齐节点再融合结构而不是简单地把多个邻接矩阵拼在一起。2.3 图融合的工程类比与边类型设计编译器优化里已经有一整套成熟的“多 IR 融合”实践。LLVM 的 pass pipeline 会把 AST、CFG、SSA 形式的 DFG 整合成统一中间表示再在同一个 IR 上做常量折叠、算子融合和死代码消除。智能合约漏洞检测的图融合思路与之相似但有一个关键差异不存在统一的编译器 IR必须自己定义融合图的边类型。我在实践中会定义这样一个 schema它决定了后续 GNN 能学到的语义上限{ node_types: [contract, function, statement, expression, variable], edge_types: { AST_PARENT: {source: statement, target: expression}, NEXT_STMT: {source: statement, target: statement}, DEF_USE: {source: variable, target: expression}, CALLS: {source: function, target: function}, STATE_WRITE: {source: function, target: variable}, COND_JUMP: {source: statement, target: statement} } }节点类型里contract和function是全局实体statement和expression来自 ASTvariable来自 DFG。边类型里AST_PARENT是语法父子关系NEXT_STMT是控制流顺序DEF_USE是数据流依赖CALLS和STATE_WRITE是跨函数语义。一张融合图同时具备这六类边GNN 才能同时感知“语法、顺序、数据、调用、状态”五个维度的信息。3. 从 Solidity 源代码生成 AST、CFG 与 DFG3.1 用 solc 把 .sol 文件解析成 AST JSON拿到 Solidity 源码之后第一步是让编译器的前端帮我们生产标准 AST而不是自己写 lexer。Solidity 0.8.x 的--ast-compact-json选项可以直接输出紧凑格式的 AST每条节点带id、nodeType、src源代码位置偏移几个关键字段。solc --ast-compact-json --ast-output-dir ./ast ./contracts/Vulnerable.sol命令说明--ast-compact-json输出压缩后的 JSON 格式体积比完整格式小并且所有字段都是扁平结构方便直接用 Python 的json模块加载--ast-output-dir指定输出目录./contracts/Vulnerable.sol是源文件路径。解析完成后可以用jq验证一下节点结构{ id: 42, nodeType: FunctionCall, src: 120:20:0, expression: {id: 41, nodeType: MemberAccess}, arguments: [] }src字段是起始位置:长度:文件索引后面做跨图节点对齐时我一般会拿这个偏移量做兜底匹配。注意 solc 版本不同输出的字段名有细微差异0.8.18 之后还支持--ast-compact-json --stop-after parsing跳过编译只做语法解析速度会快很多。3.2 从 AST 推导 CFG按语句序列生成基本块与跳转边AST 本身不包含跳转关系但控制流的逻辑隐藏在语句节点里。我常用的方式是写一个轻量 AST visitor从函数体开始遍历针对IfStatement、WhileStatement、ForStatement、FunctionCallrequire/revert四类节点单独生成跳转边。import json from collections import defaultdict def build_cfg(ast_root): edges [] node_type_map {} def walk(node, prev_stmt): node_type node.get(nodeType) node_id node.get(id) node_type_map[node_id] node_type if node_type IfStatement: # 在条件块之后生成两个分支边 edges.append((prev_stmt, node_id, COND_JUMP)) true_body node.get(trueBody) false_body node.get(falseBody) if true_body: walk(true_body, node_id) if false_body: walk(false_body, node_id) elif node_type FunctionCall: # require/revert 是终止块common 做法是加一条自环边 called_name extract_member_name(node) if called_name in (require, revert): edges.append((prev_stmt, node_id, COND_JUMP)) return prev_stmt # 默认按顺序连接 for child in node.get(children, []): walk(child, node_id) return prev_stmt walk(ast_root, -1) return edges, node_type_map代码逻辑说明prev_stmt记录上一个语句节点 id每次遇到IfStatement就生成条件跳转边到条件表达式节点随后分别遍历 true/false 分支require和revert被当作终止块处理表示该路径不会继续向后传播其他节点一律按照NEXT_STMT的顺序连边。这个实现的精度足够支撑漏洞检测论文的实验需求。参数node_id来自 AST 的id字段需要保证与后续 DFG 节点的对齐口径完全一致。3.3 从 AST 推导 DFG 与跨图节点对齐DFG 的核心是从变量声明、赋值、引用关系中提取 def-use 对。我在 Solidity AST 上做两趟扫描第一趟收集所有变量声明节点和赋值表达式的左值第二趟收集所有 Identifier 引用把引用位置和最近的 def 位置连成边。跨图节点对齐是整套方案最关键的步骤。三种图各自产出的节点 id 原本不互通常见做法是以 AST 的id作为主键把 CFG 节点的statement_id和 DFG 节点的expression_id都映射到同一套 id 上。图类型节点 id 来源对齐字段对齐策略ASTsolc 的id字段node_id权威主键CFGAST 语句节点 idstatement_id直接复用 AST idDFG表达式节点 idexpression_id扁平表达式内引用 AST id新表达式用src偏移匹配这三列映射关系最终会写进融合图的node_map字典。对齐时要注意一个常见的坑DFG 遍历过程中生成的临时表达式节点比如a b的中间结果在 AST 里没有直接对应节点必须用src位置做区间匹配落到最近的父表达式节点上。4. 图融合与 GNN 训练从 HeteroData 到注意力融合4.1 用 PyG 的 HeteroData 承载异质融合图三种图对齐之后下一步是把它们合并成一张可由 GNN 直接消费的图结构。标准做法是用 PyTorch Geometric 的HeteroData它原生支持不同节点类型和不同边类型正好对应前面 schema 里的 node_types 和 edge_types。from torch_geometric.data import HeteroData data HeteroData() data[statement].x statement_features # [num_statement_nodes, feat_dim] data[expression].x expression_features # [num_expr_nodes, feat_dim] data[function].x function_features # [num_func_nodes, feat_dim] data[statement, NEXT_STMT, statement].edge_index next_stmt_edges data[statement, COND_JUMP, statement].edge_index cond_jump_edges data[expression, DEF_USE, expression].edge_index def_use_edges data[function, CALLS, function].edge_index call_edges参数说明statement_features是语句节点的初始特征矩阵行数必须和该类型节点总数一致否则在训练时维度会报错data[statement, NEXT_STMT, statement]的元组结构是 PyG 对异质边的标准索引方式三元组分别代表源节点类型、边类型、目标节点类型。特征矩阵维度不一致时需要为每种节点类型设置独立的输入维度。特征初始化我一般用三层拼接节点类型 one-hot 编码、AST 子节点类型统计直方图、以及源码片段经过 token embedding 后的向量。如果机器配置允许也可以把函数级代码用 CodeBERT 编码后作为函数节点特征但大多数毕业设计场景下一层 token embedding 已经够用。4.2 GNN 模型选型与融合层的注意力机制模型选型直接影响实验结果的解释力。对融合异质图我推荐的基线是三组对比GCN、GAT、HGT。模型最适合的融合图结构参数量注意点GCN节点类型同质化程度高小对跨类型边贡献权重不敏感GAT需要区分重要边的场景中attention head 数量影响最大HGT节点和边类型都丰富的图大训练需要较长收敛时间我的建议是主模型用 GAT加一个自定义的跨类型注意力融合层。图融合最容易踩的坑是把多种图的邻接矩阵直接拼接成一个大矩阵这样模型的感受野和语义关系是错乱的因为同一位置的节点可能同时属于 AST 和 CFG拼接后特征会被错误聚合。import torch import torch.nn as nn from torch_geometric.nn import GATConv class CrossTypeGATLayer(nn.Module): def __init__(self, in_dim, hidden_dim, n_heads, n_types): super().__init__() self.attn nn.MultiheadAttention(in_dim, n_heads, batch_firstTrue) self.type_gate nn.Linear(in_dim n_types, in_dim) def forward(self, x, edge_index, type_context): # 对每种边类型分别做一次消息传递 gate torch.sigmoid(self.type_gate(torch.cat([x, type_context], dim-1))) x x * gate x, _ self.attn(x, x, x) return x这里的type_gate是一个类型门控层type_context是边类型的 one-hot 编码作用是在消息传递前根据边的类型动态调节节点特征的保留比例让 AST 父子边的传播和 DEF_USE 边的传播互不干扰。nn.MultiheadAttention负责在多个特征子空间上捕捉不同图结构之间的交互。最后输出接入一个分类头用 softmax 输出合约有漏洞的概率。4.3 训练配置与类别不平衡下的损失改造漏洞样本和正常合约的比例通常差距很大直接用交叉熵训练会让模型学会“全部判正常”的懒惰策略。我在训练时会同时改造损失函数和评估方式。import torch.nn.functional as F def focal_loss(logits, labels, alpha0.75, gamma2.0): ce_loss F.cross_entropy(logits, labels, reductionnone) pt torch.exp(-ce_loss) focal alpha * (1 - pt) ** gamma * ce_loss return focal.mean()参数说明alpha控制正负样本权重设为 0.75 表示把正类有漏洞的权重抬高gamma是难易样本调节因子设为 2.0 时模型会显著降低已经预测正确的简单样本的 loss 贡献。训练循环里除了常规的optimizer.zero_grad()和loss.backward()之外我还会在验证集上做早停超参数推荐值说明hidden_channels128过小欠拟合过大容易在少样本场景过拟合attention heads8GATConv 的heads参数dropout0.3缓解小数据集过拟合learning rate1e-3配合 AdamW训练更稳epochs200配合早停验证集 F1 连续 20 epoch 不上升就停评估时按漏洞类型分别计算 precision、recall、F1而不是只报一个 total 指标。只报总 F1 会掩盖重入检测率低但整体样本多的问题严重拉低论文的说服力。训练完成后把 best model 的权重保存下来后续做可解释性分析要用。5. 用可解释性把图融合变成论文里的 case study5.1 用 EdgeMask 定位贡献最大的边图融合做得再好如果输出只是一个概率值答辩时很难讲清楚模型“为什么”判定某段代码有漏洞。一个有效的做法是对融合图的边做 mask 优化找到对预测概率影响最大的边子集。edge_mask torch.nn.Parameter(torch.ones(num_fused_edges)) optimizer torch.optim.Adam([edge_mask], lr0.05) for step in range(500): optimizer.zero_grad() logits model_with_edge_mask(data, edge_mask) loss logits[target_node_id].sum() loss.backward() optimizer.step()edge_mask初始化为全 1表示所有边都保留优化目标是让目标节点的预测分数尽量降低从而暴露模型推理时真正依赖的边。训练结束后按edge_mask数值从大到小取前 20 条边这些边就是模型决策的支撑结构通常能直接对应到一条真实的攻击路径。5.2 将关键子图映射回源代码行拿到关键边之后通过节点对齐表把边两端的节点 id 映射回 AST 的src字段进而还原到具体源码行。这个输出样例是论文里非常有说服力的证据{ vuln_type: reentrancy, trigger_chain: [ {stmt: call.value{gas: 5000}(), line: 82, edge: CALLS}, {stmt: balances[msg.sender] - amount, line: 91, edge: STATE_WRITE}, ], explanation: 外部调用被标记为关键边说明模型学到了先调用后更新的先导模式 }把这条链路写进毕设论文的案例分析章节时我一般会同时放融合图的子图可视化截图和上面这个 JSON 输出评审能直接从源码看到漏洞触发链。图融合在这个场景下的价值在于它不只是管用的特征工程还天然提供了从“图结构”到“代码语义”的追溯路径这比单独用 AST 或单独用深度学习分类器更接近审计员的真实推理过程。当你把一次重入攻击拆解成“外部调用边 → 状态写入边”这条最短路径时模型就不再是一个黑盒而是变成了一个能在合约源码里指路的静态分析助手。本文还有配套的精品资源点击获取