一、智能合约上线前的“安全体检”到底查什么
很多做区块链开发的朋友都有过这种经历:好不容易写好的智能合约,刚上线没几天就出问题,要么被人偷了资产,要么功能失控,最后要么赔钱要么丢口碑。其实大部分问题都不是不可避免的,只是上线前的安全检查没做到位。今天咱们就说两个最容易踩的大坑——自毁函数和后门铸币,还有整个检查流程的必查细节和好用的工具。
先给大家提个醒,所有示例都用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这个地址调用,就能随便造币。这种后门非常隐蔽,普通用户根本发现不了,只有仔细看代码才会注意到。
那怎么识别这种后门?核心就是检查所有函数的权限控制和变量限制,尤其是那些名字很奇怪、看起来没用的函数,比如secretMint、hiddenMint这种。
二、智能合约安全验收的全流程必查细节
知道了两个大坑,接下来我们说整个验收流程的必查细节,不管你是开发自己查,还是找第三方审计,这些步骤都不能少。
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 {}
}
onlyOwner是Ownable合约提供的修饰符,加了之后就自动检查调用者是不是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 注意事项
第一,不要随便复制别人的代码,很多项目都是因为复制了有漏洞的代码出问题的,复制过来的代码必须自己检查一遍;第二,权限控制要最小化,比如一个函数只需要某个地址能调用,就不要给所有地址权限;第三,上线前要做模拟攻击,比如找专业的安全团队做渗透测试,模拟黑客攻击合约,看能不能成功;第四,合约上线后还要持续监控,比如监控有没有异常的转账、有没有调用敏感函数,一旦发现问题,及时处理。
五、总结
智能合约的安全不是小事,上线前的检查是最后一道防线。大家只要记住,自毁函数必须加权限,不能有后门铸币,按照全流程检查细节,用标准的工具链,就能避免大部分常见问题。
最后给大家提个醒,安全检查是一个持续的过程,不是上线前做一次就完了,上线后还要持续关注新的漏洞和攻击方式,及时升级合约。希望大家的合约都能安全上线,顺利运行。
评论
围绕“上线审计隐患清单:自毁函数与后门铸币的代码级识别,智能合约安全验收的全流程必查细节与标准工具链”参与讨论