1背景首先我们需要明确智能合约以及智能合约审计的概念。智能合约(SmartContract)是一种部署在区块链上的程序,其代码和逻辑一旦部署便不可随意更改。智能合约可以自动执行预先设定的规则,比如资金转账、资产管理、DeFi协议逻辑等。由于智能合约直接控制链上资产,一旦代码存在漏洞,
首先我们需要明确智能合约以及智能合约审计的概念。
智能合约(Smart Contract)是一种部署在区块链上的程序,其代码和逻辑一旦部署便不可随意更改。智能合约可以自动执行预先设定的规则,比如资金转账、资产管理、DeFi 协议逻辑等。
由于智能合约直接控制链上资产,一旦代码存在漏洞,可能会导致严重的资金损失。例如历史上著名的 DAO 攻击事件,黑客利用重入漏洞盗取了数千万美元的以太坊资产。因此,为了保证智能合约的安全性、可靠性以及合规性,需要对智能合约进行审计。
智能合约审计是一个全面的审查过程,仔细检查合约代码,以发现安全漏洞、编码错误和低效率,旨在确定纠正措施以提高合约的安全性和效率。此步骤对于区块链应用程序的完整性和功能至关重要,因为智能合约的不可变性质意味着它们的代码一旦部署就极难回滚。代码中的错误或漏洞无法在部署后纠正,否则会产生重大成本和延迟,从而需要开发和部署新版本。
下文将从审计关注面、技术方法以及审计工具三个维度展开。
关于智能合约审计的核心方面,我们可以先阅读一些专业机构的审计报告,以下列几篇作为参考:
总体来说,审计的目标是确认合约的安全性、业务逻辑正确性、合规性、上线可行性;评估并提出可执行的修复建议。
审计的核心维度应覆盖以下必要方面:
1)重入(Reentrancy)
具体的重入攻击实例,以及开发人员如何避免重入、审计人员如何检测重入可以参考漫雾安全团队的一篇教学博客:重入漏洞
2)访问控制/权限错误(Access Control)
看一段具体的solidity合约代码实例:
function initOwner(address _owner) public{
owner=_owner;
}
在进行管理员初始化的时候,并没有对函数设置合理的权限,使用public函修饰的函数,攻击者可以自己调用此函数使自己成为管理员,从而进行管理员的操作比如提现等。
可以在合约部署的时候,直接初始化管理员,因为构造函数仅仅会被调用一次,不会发生权限的更改。
// 部署时立刻生效,且只会执行一次
constructor(){
owner = msg.sender;
}
3)整数溢出/下溢(Overflow/Underflow)
4)未检查外部返回(Unchecked External Call)
在solidity中当我们与另一个合约交互时,并非所有外部调用在失败时都会自动“回滚”(Revert)整个交易。
让我们来看一个典型的有漏洞的 withdraw
函数:
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// 漏洞所在之处!
// 如果 token.transfer() 失败并返回 false,代码会继续执行
token.transfer(msg.sender, amount);
// 无论转账是否成功,用户的内部余额都会被清零
balances[msg.sender] -= amount;
}
在上面的代码中,如果 token.transfer(msg.sender, amount)
因为某种原因(例如,合约本身没有足够的代币可供转出)执行失败,它会返回 false
。但由于代码没有检查这个返回值,程序会继续执行 balances[msg.sender] -= amount;
。
结果就是: 用户的代币从未真正到账,但他们在合约中的余额记录却被扣除了。用户的资金被永久地锁在了合约的错误状态里。
解决方法:使用 require()
语句来包装外部调用,确保其返回值是 true
。如果返回 false
,require()
会使整个交易回滚。
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// 正确的做法:检查返回值
bool success = token.transfer(msg.sender, amount);
require(success, "Token transfer failed"); // 如果失败,交易会回滚
balances[msg.sender] -= amount;
}
5)交易顺序依赖 / 抢先(TOD / Front-running)
顾名思义,在插入 TOD 中,将恶意行为者的交易插入到合约和用户的交易之间。来看一段代码实例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract numGuess {
address public winner;
function guess(uint _guess) external {
require (_guess == 9841984198);
winner = msg.sender;
}
}
玩家猜对数字并作为交易提交→作弊者在内存池中看到正确的猜测并以更高的Gas提交→作弊者的交易被优先处理,并被视为赢家。
解决方法:使用commit reveal hash 方案—不是直接传输答案,而是传输盐,地址和答案的哈希。
contract numGuess {
address public winner;
mapping (address => bytes32) public playerHashes;
function storeHash(bytes32 _hash) external {
playerHashes[msg.sender] = _hash;
}
function guess(uint _answer, uint _salt) external {
require (keccak256(abi.encodePacked(_answer, _salt, msg.sender)) == playerHashes[msg.sender]);
require (_answer == 9841984198);
winner = msg.sender;
}
}
创建一个包含答案和盐的交易。合约将重新计算哈希值,如果与之前的哈希值匹配,则发布答案。别人即使看到你的哈希,也不能提前猜出答案。
6)时间戳依赖
block.timestamp
等可被微控时间源合约实例分析:
contract TimeGame1{
uint public lastBlockTime;
function lucky() public payable{
require(msg.value == 100 wei);
require(lastBlockTime != block.timestamp); //block.timestamp获取当前区块的时间戳
lastBlockTime = block.timestamp;
if(lastBlockTime % 10 == 5){
msg.sender.transfer(address(this).balance);
}
}
}
由于矿工有个0~900s的任意设置时间戳的权限,导致矿工可以非常轻易的来设置满足交易的时间戳。普通
用户可以自己写一个攻击合约来调用lucky(),也是可以自由设置满足交易的时间戳。
解决方法:需要进行资金锁定等操作时,如果对于时间操纵比较敏感,建议使用区块高度、近期区块平均时间等数据来进行资金锁定,这些数据不能被矿工操纵。
7)预言机操纵(Oracle Manipulation)
操纵预言机最常见的方法之一是通过现货价格操纵。这个概念很简单,但却具有灾难性。许多协议从 DEX 获取资产的“当前”价格,以评估存款或触发交易。但是,可以使用闪电贷暂时操纵此价格。
闪电贷允许用户借入大量加密货币而无需抵押品,只要他们在单个交易中偿还贷款即可。攻击者通过提取闪电贷来推高或抛售流动性池中资产的价格来利用这一点,从而在短时间内大幅改变其价格。
解决方法:多元化是关键。 依赖多个数据源可以大大增加操纵的成本和难度。使用来自各种预言机的中位数价格而不是单一馈送可以减少错误或被操纵来源的影响。
时间加权平均价格 (TWAP) 和成交量加权平均价格 (VWAP) 可以有效地最大限度地减少短期操纵。但是,它们需要正确的配置,例如强制定期更新和确保足够的观察窗口。
这一部分就是做一个逐行代码的检验,主要去检查业务是否符合逻辑,是否具备可行性。
1)规格一致性(Spec Compliance)
示例:提现应返回实际扣费后金额,但实现遗漏扣费逻辑(与规格不符)
// 规格:提现需扣除 1% 协议费并打给 feeCollector
contract Vault {
IERC20 public asset;
address public feeCollector;
uint256 public feeBps = 100; // 1%
mapping(address => uint256) public balance;
function withdraw(uint256 amt) external {
require(balance[msg.sender] >= amt, "insufficient");
balance[msg.sender] -= amt;
// ❌ 错误:直接全额转给用户,未扣费 —— 与规格不符
asset.transfer(msg.sender, amt);
}
}
2)状态机与生命周期(State Machine)
3)会计与资产不变量(Accounting Invariants)
4)参数与边界条件(Thresholds & Bounds)
示例:阈值比较用错 ≥ 与 > 导致“刚好等于阈值”时行为错误
contract Threshold {
uint256 public minCollateral = 1000;
function open(uint256 collateral) external {
// ❌ 需求:collateral 必须 ≥ minCollateral
// 错误:用了 ">",导致等于阈值被拒
require(collateral > minCollateral, "too small");
}
}
5)费用与收益分配(Fees & Rewards)
6)时间相关规则(Timelock / Vesting / Cooldown)
7)权限与治理对业务的约束(Governance Impact on Business)
8)可升级与迁移的业务兼容性(Upgrade & Migration Semantics)
9)定价与清算逻辑(Pricing & Liquidation)(金融类合约强制项)
10)原子性、幂等性与失败处理(Atomicity & Idempotence)
代码实例:
function swap(...) external {
uint256 out = getOut(amt);
require(out > 0, "no out");
// 尽量确保失败自动 revert(如使用 safeTransfer / 需要则前置检查)
IERC20(tokenIn).transferFrom(msg.sender, address(this), amt);
IERC20(tokenOut).transfer(msg.sender, out); // 若失败自动 revert,保持原子性 ✅
}
核心思想:使用这些智能合约审计的技术手段来检测出上述漏洞与业务逻辑的问题。
1)定义:静态分析的核心思想是在执行前识别漏洞。在不部署或执行合约的情况下进行安全审查。它通常是任何彻底的智能合约审计的第一步。通过检查代码,我们可以识别逻辑、结构和安全实践中的漏洞,这些漏洞可能会在编译时破坏或损害实现。
2)静态分析关键技术
程序语义
是指研究程序如何执行以及它们的含义和。它提供了一个正式的框架来理解程序的行为,确保软件的行为符合预期。
在智能合约审计中,能够进行安全证明,例如验证函数始终返回正确的值帮助形式化验证正确性属性,确保合约执行与预期一致。
我们来看一个代码实例:
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
msg.sender.call{value: amount}(""); // 外部调用
balances[msg.sender] -= amount; // 状态更新太晚
}
语义分析会发现状态更新顺序不合理,存在重入风险。
数据流分析
是指跟踪变量在合约中的定义 → 传递 → 使用的全过程,分析它在不同执行路径中的值的变化和依赖关系。
在智能合约审计中,数据流分析会检查来自msg.sender
、msg.value
、tx.origin
、block.timestamp
等外部数据是否直接影响转账金额、权限判断等。
我们来看一个代码实例:
function setOwner(address _newOwner) public {
owner = _newOwner; // 没有权限检查
}
数据流分析会发现`_newOwner`来自未经验证的`msg.sender` 调用路径,存在权限问题。
过程间分析
是指跨函数、跨合约追踪变量和控制流,分析函数间的调用关系和副作用。
在审计中,过程间分析会发现一个合约的withdraw()
依赖另一个合约的返回值,而另一个合约可被攻击者替换(比如通过delegatecall
或代理合约);有的合约部署后需要调用initialize()
才设置owner
,过程间分析能找到从构造函数到实际使用的调用链,判断是否存在未初始化就可被外部调用的风险。
3)局限性:虽然静态分析非常适合尽早发现问题,但它并不完美。它倾向于产生误报,这意味着它有时会标记实际上无法利用的问题。此外,它没有告诉我们合约在真实的区块链环境中的行为方式。一些漏洞只有在合约方法被执行时、与其他智能合约交互时、发生可变区块链环境状态时或存在不可预测的用户输入时才会出现。
1)定义:动态分析核心思想是将合约投入测试。通过自动或半自动生成随机或结构化输入(包括跨交易、跨合约交互),在模拟区块链环境中触发异常行为。
2)动态分析关键技术
符号执行
不直接用具体输入跑程序,而是用符号变量代替输入值,沿着所有可能的执行路径去推导程序的逻辑和条件约束。
在审计中,符号执行工具(如Mythril)会推导出执行到外部调用后,控制流可能回到当前函数的路径条件,从而发现未保护的重入点;符号执行会枚举导致 a + b 或 a - b 出错的输入条件,并直接给出攻击用例。
基于可满足性模理论(SMT)
基于 SMT 的技术涉及使用自动推理通过求解逻辑约束来分析智能合约。 这些技术利用 SMT 求解器通过将合约逻辑编码为数学公式来验证合约的正确性、安全性和功能行为。
SMT 求解器是一种工具,用于确定给定的逻辑公式是否可满足,这意味着是否存在使公式成立的值分配。
在审计中,比如权限判断包含多个与/或条件:
require(msg.sender == owner || (whitelist[msg.sender] && amount < 100));
SMT 求解器可以自动解出`whitelist[msg.sender] = true`且`amount = 99`时无需是`owner` 也能调用的情况。
混合执行测试(Concolic / Hybrid Execution Testing)
混合执行测试(具体和符号执行的组合)是一种通过分析实际输入(具体执行)和抽象符号值(符号执行)来自动生成测试用例的技术。 它对于检测边缘情况和最大化代码覆盖率特别有用。
在审计中,Fuzzing 随机生成输入 → 符号执行分析路径条件 → 根据条件生成更有针对性的输入,覆盖深层逻辑分支。例如,某合约函数只有在amount == totalSupply / 2
时触发漏洞,纯随机 Fuzz 很难撞到,但混合执行能推导条件,直接生成这个输入。
3)局限性
虽然动态分析功能强大,但也面临着挑战。它需要更多的计算资源和时间,因此与大型代码库的静态分析相比,它的可扩展性较差。此外,我们无法测试所有可能的执行路径,这意味着某些漏洞可能仍未被检测到。
可以参考阅读这篇前沿文章:AI-Assisted Security Audits
背景:在如今,大模型时代的到来,区块链安全审计的格局正在发生显著变化。人工智能正成为我们审计工具库中越来越有价值的补充。传统上,安全审计师依赖于 Mytrhil、Slither 和 Oyente 等静态分析工具的组合,以便检测已知漏洞模式,以及像 Echidna 这样的动态分析工具进行基于模糊测试的漏洞发现。尽管这些工具仍然是审计过程的基础,但审计师们开始在安全过程中添加按照 AI 驱动的工具作为额外的辅助。
以下我分析几个常用的AI工具:cursor、Solidity Sentinel、LightChaser、Peculiar
1)cursor(官网)
在审计中,比如查看多合约项目(如 DeFi 协议)时,审计人员可以使用 Cursor 问复杂问题,例如“这段逻辑如何与其他模块交互?”Cursor 能抓取当前项目文件上下文反馈答案,同时生成控制流/数据流图进行辅助理解。
在本地安装后,可以看到它可以导入代码,并且在右上方选择模型。Cursor 推出的新功能 Bug Finder 取代了旧的“审查”功能,并在误报方面表现得比之前更好。此功能逐行扫描你对提交分支所做的更改,并检查是否引入了任何漏洞。这在审计的后期阶段非常有用,当你通过客户端来验证纠正措施或漏洞修复时。
2)Solidity Sentinel(官网)
3)LightChaser(官网)
4)Peculiar(官网)
推荐看一下这篇汇总审计工具的最新博客:行业领先智能合约审计和安全工具
在日常的智能合约审计中,可以借助一些审计工具来帮助我们快捷、高效、准确得进行安全审计。
下文将介绍几种行业内领先的几款智能合约审计和安全工具。
1)介绍:Slither(仓库地址)是Trail of Bits(Crytic)推出的开源静态分析框架。
2)本地部署使用:我的电脑环境是Mac系统,使用brew可以用一行命令快捷安装工具包
brew install slither-analyzer
安装成功:
使用命令 slither path/to/YourContract.sol
即可进行审计,注意这里需要本地有solc环境,如果没有也可以用brew安装。如图,可以看到检测结果打印到终端:
总结:Slither 支持数十种漏洞检测器,还提供调用图、继承关系、优化建议等分析能力。
1)介绍:Echidna(仓库地址)是一款智能合约安全工具,利用属性基础模糊测试通过测试合约以用户定义的谓词发现漏洞。由 Crytic / Trail of Bits 社区维护,擅长用合约 ABI 生成调用序列并尝试证伪用户定义的不变量或 Solidity 断言。它能在很短时间内找到违反断言/不变量的输入序列。
2)能发现的典型问题:
3)使用方法:
brew install echidna
echidna SimpleStorage.sol --test-mode assertion
,会进入一个审计结果界面1)介绍:Diligence Fuzzing(仓库地址)是 ConsenSys Diligence 提供的模糊测试工具链,特色是把“规范/属性”用 Scribble 注释写入合约,工具会将合约 instrument(用 Scribble 标注)并运行 fuzz campaign(支持 Smart/Manual/Foundry 模式),同时提供云 API(需 API Key)与本地 CLI。
2)安装 / 快速运行(示例)
pip3 install diligence-fuzzing
或从 GitHub Clone 并pip3 install .
。GitHub# 用 Scribble 在源码加注释(示例见 docs)
fuzz arm contracts/*.sol # instrument 源码(Scribble)
export FUZZ_API_KEY=<your_key> # 若使用 Smart/云模式
fuzz run # 启动 fuzz campaign
# 或:fuzz forge test (将 Foundry 单元测试提交为 fuzz campaign)
3)局限:需要先用 Scribble 明确定义属性;若属性写得不全面就可能漏掉问题;Smart 模式需要 API Key(云服务)
1)介绍:这是一款在线的审计工具,提供智能合约的静态分析和 AI 驱动的业务逻辑审计,支持从 GitHub、区块浏览器或文件上传进行扫描。
2)使用方法:
3)对比:remix在线编辑器其实本身也有审计功能,在编译界面选择run SolidityScan,等待大概30S即可在终端在线查看结果:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!