一、智能合约上线前的“安全体检”到底查什么

很多做区块链开发的朋友都有过这种经历:好不容易写好的智能合约,刚上线没几天就出问题,要么被人偷了资产,要么功能失控,最后要么赔钱要么丢口碑。其实大部分问题都不是不可避免的,只是上线前的安全检查没做到位。今天咱们就说两个最容易踩的大坑——自毁函数和后门铸币,还有整个检查流程的必查细节和好用的工具。

先给大家提个醒,所有示例都用Solidity(以太坊智能合约开发的主流语言),大家跟着看的时候不用怕,我会把每一行代码的意思都讲清楚。

1.1 第一个大坑:自毁函数的“隐形炸弹”

自毁函数本来是用来清理没用的合约、把合约里的钱转走的,但如果用得不好,就会变成别人能随便触发的炸弹。举个例子,先看一段有问题的代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BadSelfDestruct {
    // 这个函数没有做权限控制!任何人都能调用
    function destroyContract() external {
        // 自毁函数,会把合约里的所有ETH转给调用者
        selfdestruct(payable(msg.sender));
    }

    // 合约里存的钱,随便谁都能通过上面的函数转走
    receive() external payable {}
}

大家看这段代码的问题在哪?就是destroyContract函数前面加的是external,没加权限限制。也就是说,只要知道这个合约的地址,任何人都能调用这个函数,把合约里的钱全转走。我之前见过一个项目,因为这个问题,刚上线3小时就被人转走了200多个ETH,损失不小。

那正确的自毁函数应该怎么写?得加权限控制,比如只有合约的创建者(通常叫owner)才能调用。给大家看改完的版本:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GoodSelfDestruct {
    // 定义合约的创建者地址
    address public owner;

    // 构造函数,合约部署的时候会自动运行,把部署者设为owner
    constructor() {
        owner = msg.sender;
    }

    // 加了权限控制:只有owner才能调用
    function destroyContract() external {
        // 检查调用者是不是owner,不是的话直接报错
        require(msg.sender == owner, "Only owner can destroy");
        selfdestruct(payable(owner));
    }

    receive() external payable {}
}

这段代码就安全了,因为加了require(msg.sender == owner),只有部署合约的人才能触发自毁。

1.2 第二个大坑:后门铸币的“暗门”

后门铸币就是合约里偷偷留了个只有开发团队能用的铸币功能,别人不知道,开发团队自己能随便造币。这是非常严重的诚信问题,也是安全隐患。给大家看一段藏了后门的代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract HiddenMint {
    // 总供应量变量
    uint256 public totalSupply;
    // 代币余额映射:地址对应余额
    mapping(address => uint256) public balanceOf;
    // 定义一个只有合约创建者知道的“暗门地址”
    address public secretAdmin = 0x1234567890123456789012345678901234567890;

    // 正常的铸币函数,公开的,有总量限制
    function mint(address to, uint256 amount) external {
        require(totalSupply + amount <= 1000000 ether, "Exceed max supply");
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    // 藏起来的后门铸币函数,没有总量限制,只有secretAdmin能调用
    function secretMint(address to, uint256 amount) external {
        require(msg.sender == secretAdmin, "Not secret admin");
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    // 转账函数,正常的
    function transfer(address to, uint256 amount) external {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
}

大家看这段代码,表面上有一个公开的mint函数,限制了总供应量不能超过100万枚,但背地里藏了一个secretMint函数,没有总量限制,只要是secretAdmin这个地址调用,就能随便造币。这种后门非常隐蔽,普通用户根本发现不了,只有仔细看代码才会注意到。

那怎么识别这种后门?核心就是检查所有函数的权限控制和变量限制,尤其是那些名字很奇怪、看起来没用的函数,比如secretMinthiddenMint这种。

二、智能合约安全验收的全流程必查细节

知道了两个大坑,接下来我们说整个验收流程的必查细节,不管你是开发自己查,还是找第三方审计,这些步骤都不能少。

2.1 第一步:代码规范检查

首先得看代码写得规不规范,不规范的代码很容易出问题。比如变量名是不是有意义,注释是不是清楚,函数是不是都加了权限控制。比如刚才的secretMint,变量名起得太奇怪,一看就有问题。

给大家举个反例,比如有个函数叫_a(),里面啥注释都没有,那这个函数大概率有问题,得重点查。

2.2 第二步:权限控制检查

所有函数都得检查权限,不能有随便谁都能调用的敏感函数。比如铸币、转账、销毁、修改合约参数这些函数,必须加权限控制。

这里给大家介绍一个好用的小技巧:用Ownable合约,这是OpenZeppelin提供的标准合约,专门用来管理权限,不用自己写。比如刚才的自毁函数,用Ownable改的话会更简单:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 导入OpenZeppelin的Ownable合约
import "@openzeppelin/contracts/access/Ownable.sol";

contract GoodSelfDestruct is Ownable {
    // 只有owner能调用的自毁函数
    function destroyContract() external onlyOwner {
        selfdestruct(payable(owner()));
    }

    receive() external payable {}
}

onlyOwnerOwnable合约提供的修饰符,加了之后就自动检查调用者是不是owner,不用自己写require,既简单又不容易出错。

2.3 第三步:逻辑漏洞检查

除了权限,还要检查业务逻辑有没有漏洞。比如转账的时候有没有检查余额够不够,铸币的时候有没有检查总量限制,有没有整数溢出的问题(不过Solidity 0.8版本之后会自动检查整数溢出,不用自己写了)。

再举个逻辑漏洞的例子,比如一个投票合约,要求只有已经投票的人才能撤回投票,结果代码写成了只有没投票的人才能撤回,这就是逻辑反了,会导致乱套。

2.4 第四步:测试验证

所有的功能都得测试,不能只看代码。比如测试自毁函数,部署合约之后,先用别人的地址调用,看是不是会报错;再用owner的地址调用,看是不是能成功自毁。测试铸币函数,正常的铸币是不是能达到总量限制,后门铸币是不是真的能随便造。

这里给大家介绍一个测试工具——Hardhat,这是专门用来测试Solidity合约的工具,非常好用。给大家看一个简单的测试脚本示例:

// 测试自毁函数的脚本,用Hardhat写的
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("GoodSelfDestruct", function () {
  it("Should only allow owner to destroy", async function () {
    // 部署合约
    const GoodSelfDestruct = await ethers.getContractFactory("GoodSelfDestruct");
    const contract = await GoodSelfDestruct.deploy();
    await contract.deployed();

    // 获取两个地址:owner和另一个地址
    const [owner, otherAddress] = await ethers.getSigners();

    // 给合约转1ETH
    await owner.sendTransaction({
      to: contract.address,
      value: ethers.utils.parseEther("1"),
    });

    // 用另一个地址调用自毁函数,应该报错
    await expect(contract.connect(otherAddress).destroyContract()).to.be.revertedWith("Only owner can destroy");

    // 用owner调用自毁函数,应该成功
    await contract.destroyContract();
    // 检查合约里的ETH是不是都转到owner手里了
    const ownerBalance = await ethers.provider.getBalance(owner.address);
    expect(ownerBalance).to.be.gt(ethers.utils.parseEther("1"));
  });
});

这个测试脚本会自动验证:不是owner的人调用自毁函数会报错,owner调用会成功,合约里的钱会转走。

三、标准工具链:不用自己瞎找,这些工具足够用

做安全检查不用自己从头写工具,有很多现成的工具链,覆盖从代码检查到测试的全流程,给大家列出来,都是免费又好用的。

3.1 代码静态检查工具:Slither

Slither是专门检查Solidity合约的静态分析工具,能自动找出权限问题、逻辑漏洞、整数溢出这些问题。用起来也简单,只要安装好,然后执行一条命令就行:

# 安装Slither(需要先装Python)
pip3 install slither-analyzer

# 检查当前目录下的合约
slither .

Slither会生成一个报告,告诉你哪里有问题,比如它会发现刚才那个没有权限控制的自毁函数,会给出提示:Missing access control for destroyContract(),非常直观。

3.2 权限检查工具:MythX

MythX是一个更强大的安全分析工具,不仅能检查静态漏洞,还能模拟攻击,比如检查有没有后门铸币的问题。它有免费版,足够个人开发者用。

3.3 测试工具:Hardhat + Ethers.js

刚才已经提到Hardhat,它不仅能写测试脚本,还能部署合约、调试合约,是现在最流行的开发工具链。Ethers.js是用来和合约交互的库,配合Hardhat用非常方便。

3.4 代码对比工具:Diffchecker

如果是升级合约,要对比新旧合约的代码变化,看有没有偷偷加了后门函数,用Diffchecker就能很方便地对比两个代码文件的差异。

四、应用场景、优缺点和注意事项

4.1 应用场景

这套检查流程和工具链,适用于所有类型的智能合约,不管是代币合约、NFT合约、DeFi合约还是游戏合约,只要是用Solidity写的,都能用。尤其是涉及到资产的合约,比如存了很多ETH、USDT的合约,上线前必须做全流程检查。

4.2 技术优缺点

先说说优点:第一,能提前发现大部分常见漏洞,避免上线后出问题;第二,工具链都是开源免费的,成本低;第三,流程标准化,不管是自己查还是找第三方,都有统一的标准,不会漏项。

再说说缺点:第一,静态检查工具只能发现已知的漏洞,新的漏洞(比如最近刚出现的某类攻击)可能发现不了;第二,测试覆盖度再高,也不能保证100%没有漏洞,比如一些复杂的逻辑漏洞,测试用例没覆盖到的话,还是会漏;第三,人工检查还是必不可少,工具不能完全代替人的判断,比如一些逻辑上的问题,工具可能识别不出来。

4.3 注意事项

第一,不要随便复制别人的代码,很多项目都是因为复制了有漏洞的代码出问题的,复制过来的代码必须自己检查一遍;第二,权限控制要最小化,比如一个函数只需要某个地址能调用,就不要给所有地址权限;第三,上线前要做模拟攻击,比如找专业的安全团队做渗透测试,模拟黑客攻击合约,看能不能成功;第四,合约上线后还要持续监控,比如监控有没有异常的转账、有没有调用敏感函数,一旦发现问题,及时处理。

五、总结

智能合约的安全不是小事,上线前的检查是最后一道防线。大家只要记住,自毁函数必须加权限,不能有后门铸币,按照全流程检查细节,用标准的工具链,就能避免大部分常见问题。

最后给大家提个醒,安全检查是一个持续的过程,不是上线前做一次就完了,上线后还要持续关注新的漏洞和攻击方式,及时升级合约。希望大家的合约都能安全上线,顺利运行。