通过 Bytecode 计算合约相似度

最近 Forta Foundation 让我帮他们做一个监控机器人，要求给定一个集合的恶意合约（Malicious Contracts），判断当前创造的合约是否与恶意合约集合中的任意一个合约相似。这几天疯狂看了十几篇论文，最终终于比较圆满的完成了这个需求。整个任务感觉非常有意思和挑战性，最终项目在这里。

Forta Network 是什么

Forta 的官网简介如下：

The leading decentralized security & operational monitoring network for wallets, developers, and investors

简单的说，Forta Network 是一个用于安全监控的分布式计算平台。开发者可以首先按照需求开发 Forta Bot，Forta Bot 里可以处理区块链上的每一条交易。然后开发者可以把 Forta Bot 上传到 Forta Network，就可以 24 小时处理区块链信息了。

比如说，开发者可以编写一个 Forta Bot 来实时监控算法稳定币是否脱锚。如果脱锚，则发出警告。还比如开发者可以编写一个 Forta Bot 来监控某个项目的合约是否变更了所有者，可以用于提前探测攻击行为。

核心困难

由于大部分上传到区块链的合约都是不开源的，所以我们不能直接对合约的源代码进行语义分析。相反，我们只能对合约的 Bytecode 进行分析。Bytecode 失去了源代码中的大量语义信息，所以不管是学术上还是工业上，对合约 Bytecode 的相似度检测都罕被探索。

解决方案

我们最终使用函数级别的相似度检测。总的来说，我们的方法的步骤如下：

反编译合约 Bytecode 到 OPcode 和 CFG。

遍历 CFG，提取合约的每一个函数，并按函数中 Basicblock 的顺序提取每一个函数中所有指令。

将一个函数的所有指令向量化到一个稠密向量。

通过函数间的相似度计算合约间的相似度。

反编译

众所周知，由于在编译的过程中程序不可避免的损失了大量的信息，完全靠谱的反编译是不存在的。但是，仅仅讲合约从 Bytecode 反编译到 OPcode 是完全可能且靠谱的。这就像在 x86 上把二进制翻译成汇编语言一样，直接照着说明书一句一句翻译就好了。所以我们反编译的第一步就是按照 EVM 文档将合约的 Bytecode 翻译成 Opcode。

之后，有了 Opcode 后，我们虽然很难直接从 Opcode 知道这个合约要干什么，但是我们可以知道合约代码中的执行顺序是怎么样的。例如，当遇到 jump 指令的时候我们大概就知道这里会有一次跳跃。我们可以通过分析代码的 Opcode 来将合约进一步变成一张 Control Flow Graph (CFG)。

在 CFG 中，每一个块被称为 Basicblock，比较官方的定义为：

In compiler construction, a basic block is a straight-line code sequence with no branches in except to the entry and no branches out except at the exit

大概意思就是，一个 Basicblock 表示这一部分代码只能从头部指令进入开始运行，然后从尾部指令结束允许。用人话讲就是一个 Basicblock 里的代码一旦被运行就是一起被运行，不会从中间跳走到另外一个 Basicblock。

有了 CFG 后，我们可以从 dispatcher() 中找到合约包含的每一个函数以及每一个函数的偏移，然后通过静态分析可以定位每一个函数包含了哪些 Basicblock。到此，大部分反编译就完成了。

提取指令

由于我们进行的是函数间的相似度计算，我们的处理自然以函数为单位。有了上一小节编译得到的 CFG 后，我们可以按照 CFG 的执行顺序遍历每一个函数所涉及的所有指令。这里，由于我们是按照 CFG 的顺序来遍历的，所以遍历后的结果天然具有一定的语义。最终，我们把每一个指令转化成字符串，不同指令以空格隔开，一个合约的指令就被提取出来了。

向量化

由于一个函数可能包含成千上万的指令，使用基于深度学习的向量化方法可能会过于复杂且低效。这里，我们使用的是基于 word2vec 的 doc2vec 来无监督地对提取的指令做向量化。如果有条件的话读者完全可以试试基于对比学习的向量化方法，例如基于对比学习的 codebert。

在训练的时候，我们使用了 slither-audited-smart-contracts 数据集，其包含大约 12 万条合约的 Bytecode。将数据集处理完后会有大约 200 万个函数供我们训练。这么多数据对于 doc2vec 来说应该是够用了，但是如果读者想尝试基于对比学习的向量化方法的话，可以试试才开源的 paradigm-data-portal 数据集。

合约相似度计算

最后，有了函数间的相似度后，我们定义合约 $C_1$ 和 $C_2$ 的相似度为：

Sim(C 1,C 2)=∑fi∈C 1 log P(fi,f 2∗)P(fi,f 2 ˉ),Sim(C_1, C_2) = \sum_{f_i \in C_1} log \frac{P(f_i, f_2^*)}{P(f_i, \bar{f_2})},Sim(C1​,C2​)=∑fi​∈C1​​logP(fi​,f2​ˉ​)P(fi​,f2∗​)​,

其中，$f_i$ 表示 $C_1$ 的第 $i$ 个函数，$f_2^$ 表示 $C_2$ 中与 $f_1$ 最相似的函数，$\bar {f_2}$ 表示 $f_i$ 与 $C_2$ 中任意函数的平均相似度. $P (f_i, f_2^)$ 和 $P (f_i, \bar {f_2})$ 是 $f_i$ 与 $f_2^*$ 和 $\bar {f_2}$ 分别相似的概率。 $P (\cdot)$ 表示为：

P(fi,fj)=1 1+e−k∗cos(fi,f 2),P(f_i, f_j) = \frac{1}{1 + e^{-k * cos(f_i, f_2)}},P(fi​,fj​)=1+e−k∗cos(fi​,f2​)1​,

其中， $k$ 是一个超参数，$cos (\cdot, \cdot)$ 是两个向量的余弦相似度。

最终，有了 $Sim (C_1, C_2)$ 后，我们计算 $C_1$ 和 $C_2$ 的相似度评分为：

score=Sim(C 1,C 2)Sim(C 1,C 1) score = \frac{Sim(C_1, C_2)}{Sim(C_1, C_1)}score=Sim(C1​,C1​)Sim(C1​,C2​)​

这个 $score$ 可以用来区分相似合约和不相似的合约。