DTeam 技术日志

Doer、Delivery、Dream

使用 Remix Analyzer 发现 Solidity 潜在问题

胡键 Posted at — Jan 10, 2023 阅读

Remix Analyzer 是 Remix IDE Solidity 静态分析插件的底层支撑库。这意味着它不仅可以用于 Remix IDE 同时也能用于其他项目。

既然 Hardhat 是现在流行的合约开发工具,如果能够跟它结合话,那当然再好不过了。在 Remix 的代码仓库中,这个测试文件显示了它的基本使用,关键代码摘录如下:

const res: CompilationResult = compile('functionParameters.sol')
const Module: any = checksEffectsInteraction
const statRunner: StatRunner = new StatRunner()
...
const reports = statRunner.runWithModuleList(res, [{ name: new Module().name, mod: new Module() }])

代码逻辑一目了然:编译代码 > 应用规则 > 输出报告。

注意:

上面代码中的 Module 对应 Remix Analyzer 的检查规则,目前支持的所有规则可以在:这个文件中找见。

由于 runWithModuleList 中可以接收多个 Module 对象,如果想应用多个规则的话,只需要初始化多个 Module 实例就行了。

封装成 Hardhat 任务

在 Hardhat 中使用 Remix Analyzer 的办法很简单:将上面的那段脚本封装成一个 Task 就行了。但直接照搬的话有个问题。如果仔细看上面的测试代码,你会发现它会先去下载一个 Solidity 的编译器,然后使用它来完成编译。在测试准备阶段有类似下面的代码:

test('setup', function (t) {
  solc.loadRemoteVersion('v0.5.0+commit.1d4f565a', (error, compiler) => {
    ...

当然,直接闷头照抄并非不行,但整个过程不太美观:

  1. Hardhat 本身就自带了编译功能,应该尽可能的去利用它,而不是另起炉灶。
  2. 基于上一步,如果能直接从编译结果中得到符合 CompilationResult 类型的数据就更完美了。这样避免了手工适配的过程。

对于 1,好办,Hardhat 直接支持在任务中调用任务,代码如下:

await hre.run(TASK_COMPILE, { quiet: true });

对于 2,也不复杂。Hardhat 暴露了属性可以方便的让开发者访问其内部各种制品(Artifact),当然也包括编译后的产物。只是这里有一点需要注意,语法分析用的并不是最终制品,而是编译后的制品。即它需要的是 artifacts/build-info 而不是 artifacts/contracts

并且,artifacts/build-info 下的 json 文件不能直接被 Remix Analyzer 所用,只是其中的一部分。通过对比 CompilationResult 类型和 json 的内容很容易发现其中关联:output 属性。因此,整个任务的逻辑框架代码如下:

const runner = new staticAnalysisRunner();

await hre.run(TASK_COMPILE, { quiet: true });
console.log("√ compiled contracts.\n");

const compileResults = await hre.artifacts.getBuildInfoPaths();
compileResults.forEach((result) => {
  const compiled = JSON.parse(fs.readFileSync(result).toString());
  const source = Object.keys(compiled.input.sources)[0];
  const modules = calculateRules(source, rulesConfig).map((Module: any) => {
    return { name: new Module().name, mod: new Module() };
  });
  const reports = runner.runWithModuleList(compiled.output, modules);
  ...
})

即:

  1. 编译
  2. 针对每个编译后的文件生成报告:解析 json > 计算规则 > 应用规则

封装成 Hardhat 插件

语法分析是一个高度可复用的任务,没有理由不进一步将其封装成一个插件。

身为 dapp 开发,如果还不清楚 Hardhat 插件为何物,那真应该好好读读文档了。利用插件,你可以方便地在项目间共享配置,当然也包括自定义的 task。

按照文档推荐的步骤:先在 Hardhat 工程中测试完任务的逻辑;再将其挪到插件工程封装成插件。现在,我们已经完成了第一步。

关于如何构建插件的过程,文档中已经说的很清楚,这里不再赘述:基于插件模板工程进行改造就行了。在实际开发过程中,个人发现有两点需要强调一下。

依赖

按照文档中总结的几条规则操作就行了。

扩展配置

在文档中并没有特别说明,而只是给出了一个示例文件的链接,但我发现需要搭配这个文件一起看才能明白其中的逻辑。读完代码,规则总结如下:

  1. 同时扩展 XxxUserConfiguration 和 XxxConfig,其中:
    • 前者新增的配置字段为可选,后者为必填。
    • 在初始化的时候基于前者生成后者,若前者没有,则考虑一个默认值。
  2. 如果要扩展 Hardhat 现有的配置项,如例子代码所做的给 paths 新添加一个属性,则需使用对应的 ProjectPathsUserConfig 和 ProjectPathsConfig 。
  3. 如果要给 Hardhat 新增一个顶层配置项,则用 HardhatUserConfig 和 HardhatConfig,如下例:
declare module "hardhat/types/config" {
  interface HardhatUserConfig {
    analyzerRules?: AnalyzerConfiguration;
  }

  interface HardhatConfig {
    analyzerRules: AnalyzerConfiguration;
  }
}

至于其余的,没啥可说的了,照着模板工程来就行了。有兴趣的话可以访问 hardhat-remix-analyzer 的代码仓库了解详情。

写在最后

除了 Remix Analyzer,还有其他两个类似工具可以关注:slithersolhint。对比之后,个人建议关注后面两个,原因在于后者提供了更多的规则,而且更新更频繁,并且 Hardhat 也提供了针对 solhint 的插件。


友情链接


相关文章