ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

合约安全评估不能只看表面结果

合约安全评估不能只看表面结果 合约安全评估不能只看表面结果“主要流程跑通”和“人工看过一遍”只说明少量样本没有立即失败不能说明资金账本、权限和外部依赖在异常顺序下仍然正确。合约测试要保存环境、输入和断言代码或依赖变化后才能重放同一结论。自动化测试、静态分析和人工审计各自覆盖一部分风险。单函数测试检查边界与权限不变性测试探索调用序列fork 测试验证某个链上快照下的集成行为经济模型和治理风险仍要单独讨论。覆盖率是观察测试范围的信号不能换算成笼统的“安全分数”。1. 合约安全评估的客观度量体系建立评估材料时可以从覆盖信息、业务不变性和外部依赖三个角度组织证据。1.1 分支覆盖率与 opcode 指令覆盖率行覆盖率容易掩盖条件组合。例如require(a || b)所在行被执行过不表示a为假、b为真等路径都经过断言。因此应关注分支和失败路径。opcode 覆盖可以补充观察字节码执行范围但覆盖率高不代表断言正确也不代表跨合约经济行为已经验证。1.2 属性不变性Invariant Check的随机游走次数不变性描述在允许状态下始终应成立的约束例如账面总额等于用户份额之和。模糊测试会探索许多输入与调用顺序但次数增加不等于覆盖完整状态空间。运行次数、序列深度和输入分布应按合约复杂度、CI 时间和历史缺陷调整并保存失败种子方便复现。1.3 Fork 主网状态下的组合攻击抗性涉及外部池、预言机或治理合约时本地 mock 容易遗漏真实接口和状态组合。主网 fork 能固定到某个区块重放调用适合检查集成假设但它不会自动模拟未来流动性、区块构建和跨链条件。测试还要主动构造价格变化、更新延迟和权限变更。2. Solidity 合约与 Foundry Fuzzing 套件实现下面用一个简化 Vault 展示 Foundry Handler 和 ghost 变量。它没有利息、份额价格、管理员或升级逻辑不应被当成生产金库实现。2.1 待测 Vault 目标合约// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import openzeppelin/contracts/token/ERC20/IERC20.sol; import openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol; import openzeppelin/contracts/utils/ReentrancyGuard.sol; contract YieldVault is ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable stakingToken; uint256 public totalSupply; mapping(address uint256) private _balances; event Deposited(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); constructor(address _stakingToken) { require(_stakingToken ! address(0), Invalid token address); stakingToken IERC20(_stakingToken); } function balanceOf(address account) external view returns (uint256) { return _balances[account]; } function deposit(uint256 amount) external nonReentrant { require(amount 0, Cannot deposit 0); uint256 balanceBefore stakingToken.balanceOf(address(this)); stakingToken.safeTransferFrom(msg.sender, address(this), amount); uint256 balanceAfter stakingToken.balanceOf(address(this)); // 抵御支持转账扣税/通缩型代币的实际到账校验 uint256 actualDeposited balanceAfter - balanceBefore; require(actualDeposited 0, Zero deposit amount); _balances[msg.sender] actualDeposited; totalSupply actualDeposited; emit Deposited(msg.sender, actualDeposited); } function withdraw(uint256 amount) external nonReentrant { require(amount 0, Cannot withdraw 0); require(_balances[msg.sender] amount, Insufficient balance); _balances[msg.sender] - amount; totalSupply - amount; stakingToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } }通过前后余额计算实际到账量可以避免入账金额高于金库收到的金额。但对扣费型代币取款者最终收到多少仍取决于代币的转账语义若产品不支持这类资产更清晰的做法是在资产准入时拒绝而不是只在存款端兼容。2.2 Foundry Invariant 模糊测试与属性断言// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import forge-std/Test.sol; import ./YieldVault.sol; import openzeppelin/contracts/token/ERC20/ERC20.sol; // 基础 Mock扣费、回调和返回值异常应使用单独的恶意 Token 测试 contract MockERC20 is ERC20 { constructor() ERC20(Mock Token, MTK) { _mint(msg.sender, 1_000_000_000 * 10**18); } function mint(address to, uint256 amount) external { _mint(to, amount); } } // Handler 模式限制随机调用的边界 contract VaultHandler is Test { YieldVault public vault; MockERC20 public token; uint256 public ghost_sumDeposits; address[] public actors; address internal currentActor; constructor(YieldVault _vault, MockERC20 _token) { vault _vault; token _token; actors.push(address(0x1111)); actors.push(address(0x2222)); actors.push(address(0x3333)); for (uint i 0; i actors.length; i) { token.mint(actors[i], 1_000_000 * 10**18); vm.prank(actors[i]); token.approve(address(vault), type(uint256).max); } } function deposit(uint256 actorIndex, uint256 amount) public { currentActor actors[bound(actorIndex, 0, actors.length - 1)]; amount bound(amount, 1, 100_000 * 10**18); vm.prank(currentActor); vault.deposit(amount); ghost_sumDeposits amount; } function withdraw(uint256 actorIndex, uint256 amount) public { currentActor actors[bound(actorIndex, 0, actors.length - 1)]; uint256 actorBalance vault.balanceOf(currentActor); if (actorBalance 0) return; amount bound(amount, 1, actorBalance); vm.prank(currentActor); vault.withdraw(amount); ghost_sumDeposits - amount; } } // 主 Invariant 测试合约 contract YieldVaultInvariantTest is Test { YieldVault public vault; MockERC20 public token; VaultHandler public handler; function setUp() public { token new MockERC20(); vault new YieldVault(address(token)); handler new VaultHandler(vault, token); // 目标 Target 仅指向 Handler targetContract(address(handler)); } /// 核心不变性 1: Vault 内代币余额必须始终大于等于总记账 supply function invariant_solvency() public view { assertGe( token.balanceOf(address(vault)), vault.totalSupply(), Vault is insolvent! Contract balance lower than totalSupply. ); } /// 核心不变性 2: 总 totalSupply 必须精确等于 Ghost 变量记录的用户存款和 function invariant_supplyEqualsGhostSum() public view { assertEq( vault.totalSupply(), handler.ghost_sumDeposits(), TotalSupply desynchronized with actual user deposits. ); } }3. 端到端 Mainnet Fork 集成测试规范对于依赖真实流动性池或预言机接口的合约可以增加主网 Fork 集成测试。区块高度必须固定否则同一用例会随链上状态变化RPC 地址通过测试环境注入不应写入仓库或日志。下面代码只展示固定区块和账户模拟。地址、区块与余额都属于该快照的前提使用前应核对末尾没有实现价格冲击和协议断言因此不能把用例名称当成已经验证的安全结论。import { expect } from chai; import { ethers, network } from hardhat; describe(Mainnet Fork Integration Security Suite, function () { const UNISWAP_V3_ROUTER 0xE592427A0AEce92De3Edee1F18E0157C05861564; const WETH_ADDRESS 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; const USDC_ADDRESS 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // 示例账户必须在固定区块上核对资产余额 const WHALE_ADDRESS 0x47ac0Fb3F2D84898e4D9E7b4DaB3C24507a6D503; before(async function () { // 强制 Fork 指定高度的主网快照 await network.provider.request({ method: hardhat_reset, params: [ { forking: { jsonRpcUrl: process.env.MAINNET_RPC_URL || , blockNumber: 18500000, }, }, ], }); }); it(为价格冲击场景准备固定的 fork 状态, async function () { const whaleSigner await ethers.getImpersonatedSigner(WHALE_ADDRESS); // 给鲸鱼账户补充 ETH 支付 Gas await network.provider.send(hardhat_setBalance, [ WHALE_ADDRESS, 0x1000000000000000000, ]); const USDC await ethers.getContractAt(IERC20, USDC_ADDRESS, whaleSigner); const initialBalance await USDC.balanceOf(WHALE_ADDRESS); expect(initialBalance).to.be.gt(0); // 后续必须执行真实池交易并断言被测协议使用的价格与清算结果。 // 如果缺少这些步骤此用例只能验证测试夹具不验证抗操纵能力。 }); });4. 落地测试策略的建立路线测试策略应当和资产、权限及依赖一起演进流水线负责稳定重放人工评审负责判断断言是否覆盖业务风险。第一步是在 CI 中运行编译、单元测试和静态分析。静态工具的告警要分类处理确认的问题修复误报记录理由和适用范围。简单要求“零 Warning”容易诱导屏蔽规则也无法替代人工判断。第二步为关键资产操作写不变性并让 Handler 只调用业务允许的入口。快速配置可在每次提交运行更深的序列放到定时任务次数与深度记录在配置中失败时保存随机种子和最小化后的调用序列。第三步针对外部协议做固定区块的 Fork 测试覆盖正常交换、价格源过期、流动性变化和权限变更。另设更新区块的兼容测试可以发现依赖升级但它与可重复的固定快照用例应分开。最后把测试未覆盖项写进评审报告例如治理密钥、跨链消息、经济攻击成本和部署参数。分层测试能提供更可靠的证据却不能证明“没有漏洞”真正有价值的是每个结论都能回到具体断言、输入和环境。
返回列表